サイトアイコン アマチュア無線局JS2IIU

Dolphinで始める文書解析とStreamlit実装入門

こんにちは、JS2IIUです。
Dolphinで画像やPDFからテキスト・表・図を抽出し、Streamlitで手早く試す手順と実装のポイントを紹介します。今回もよろしくお願いします。

概要

このページでは、ACL 2025で発表されたライブラリ「Dolphin」の概要を簡潔に説明し、ローカル環境で動かすための手順と、サンプルコードで示す streamlit_app.py を用いた実例をハンズオンで解説します。最終的に、アップロードした画像/PDFからJSON・Markdown・図を出力し、ダウンロードするまでを体験できます。

Dolphin の概観

Dolphinは “Document Image Parsing via Heterogeneous Anchor Prompting” の考え方に基づき、ドキュメント画像を高精度に解析するためのマルチモーダルモデルです。特徴は大きく分けて2点あります。

主要な出力形式

次節で内部ワークフローをもう少し技術的に解説します。

内部ワークフロー

  1. ページレベル解析(layout / reading order)
    • モデルに対してページ全体を渡し、自然な読み順に従った領域列(bbox とラベル)を生成します。出力はテキスト形式でレイアウト情報を表現することが多く、後段の要素処理に渡されます。
  2. 要素の切り出しと前処理
    • 得られた bbox を padded image の座標系にマップし、境界調整や余白トリミング(crop_margin)を行います。prepare_image で正方化パディングすることでモデル入力のサイズを統一します。
  3. 要素ごとのプロンプト生成と並列解析
    • ラベルに応じてプロンプトを決定(例: "Parse the table in the image.""Read text in the image.")。テキスト/表はバッチ化して model.chat(prompts_list, crops_list, max_batch_size=...) のように並列的に推論します。図は画像として保存し、解析は不要な場合もあります。
  4. 出力の集約とフォーマット変換
    • 要素結果を読み順でソートし、JSONファイルとして保存します。PDFの場合は全ページを統合して save_combined_pdf_results がJSONとMarkdownを生成します。

出力フォーマット例(JSON スキーマの概念例)

以下は生成される JSON の簡易例です(実際はプロジェクト固有のスキーマに従います)。

JSON
{
    "source_file": "page_1.png",
    "page_number": 1,
    "elements": [
        {"label": "p", "bbox": [10,20,400,200], "text": "これは本文の一部です", "reading_order": 0},
        {"label": "fig", "bbox": [410,20,800,500], "figure_path": "markdown/figures/page_1_figure_000.png", "reading_order": 1},
        {"label": "tab", "bbox": [10,210,800,400], "text": "|col1|col2|\n|---|---|", "reading_order": 2}
    ]
}

Markdown 変換では図を ![Figure](figures/xxx.png) のように埋め込みます。プロジェクト内の utils.markdown_utils.MarkdownConverter を使ってJSON→Markdown変換が行われます。

コード解説(主要関数とフロー)

ここでは demo_page.pyutils/utils.py の主要関数を簡潔に説明します。詳しい実装は該当ファイルを参照してください。

補助関数

実行時のチューニングと注意点

サンプル出力(抜粋)

Markdown の一部例:

Markdown
### page_1

これは本文の一部です

![Figure](figures/page_1_figure_000.png)

|col1|col2|
|---|---|
|1|2|

JSON の抜粋は前節のスキーマ例を参照してください。

前提と準備

以下を想定しています。

推奨インストール手順:

Bash
# 仮想環境を作成・有効化(例: venv)
python -m venv venv
source venv/bin/activate

# 依存をインストール
pip install -r requirements.txt
pip install streamlit

注意点:

Streamlit デモの構成要素(streamlit_app.py を参照)

streamlit_app.py は次の役割を持ちます。

重要な実装ポイント:

streamlit_app.pyのサンプルコード

Python
"""Simple Streamlit front-end for Dolphin demo_page processing.

Supports: file upload (image/pdf) -> run Dolphin processing -> show JSON/markdown/figures -> download ZIP
"""

import json
import os
import shutil
import sys
import tempfile
from pathlib import Path

import streamlit as st
from omegaconf import OmegaConf

# Import Dolphin model and processing helper
from chat import DOLPHIN
from demo_page import process_document
from utils.utils import setup_output_dirs


st.set_page_config(page_title="Dolphin Streamlit Demo", layout="wide")


@st.cache_resource
def load_model(config_path: str):
    """Load and cache the DOLPHIN model instance."""
    cfg = OmegaConf.load(config_path)
    model = DOLPHIN(cfg)
    return model


def zip_output_dir(output_dir: str) -> str:
    """Create a zip archive of the output directory and return its path."""
    base_name = tempfile.NamedTemporaryFile(delete=False, prefix="dolphin_out_").name
    # shutil.make_archive will append extension
    archive_path = shutil.make_archive(base_name, 'zip', output_dir)
    return archive_path


def main():
    st.title("Dolphin — ファイル解析デモ")

    st.sidebar.header("設定")
    default_config = "config/Dolphin.yaml"
    config_path = st.sidebar.text_input("config path", default_config)
    max_batch_size = st.sidebar.number_input("max_batch_size", min_value=1, max_value=32, value=4)
    device = st.sidebar.selectbox("device", ["cpu", "gpu"])

    uploaded = st.file_uploader("画像またはPDFをアップロード", type=["png", "jpg", "jpeg", "pdf"])

    if uploaded is None:
        st.info("左サイドバーで設定を行い、ファイルをアップロードしてください。サンプルは demo/page_imgs にあります。")
        return

    # Save uploaded file to a temp file
    suffix = Path(uploaded.name).suffix
    tmp_dir = tempfile.mkdtemp(prefix="dolphin_streamlit_")
    tmp_path = os.path.join(tmp_dir, uploaded.name)
    with open(tmp_path, "wb") as f:
        f.write(uploaded.getbuffer())

    st.sidebar.write(f"アップロードファイル: {uploaded.name}")
    st.image(tmp_path, caption="アップロード画像", use_container_width=True)

    if st.button("処理開始"):
        try:
            with st.spinner("モデルを読み込み中...(初回は時間がかかります)"):
                model = load_model(config_path)

            # Prepare output directory for this run
            out_dir = os.path.join(tmp_dir, "outputs")
            os.makedirs(out_dir, exist_ok=True)
            setup_output_dirs(out_dir)

            with st.spinner("Dolphinで解析中..." ):
                json_path, results = process_document(tmp_path, model, out_dir, max_batch_size)

            st.success("処理が完了しました")

            # Show JSON result (if available)
            if json_path and os.path.exists(json_path):
                try:
                    with open(json_path, "r", encoding="utf-8") as f:
                        data = json.load(f)
                    st.subheader("認識結果 (JSON)")
                    st.json(data)
                except Exception:
                    st.write("認識結果: 成功しましたがJSONの読み込みに失敗しました。")
            else:
                # Fall back to results object
                st.subheader("認識結果 (オブジェクト)例")
                st.write(results)

            # Show markdown if exists
            md_dir = os.path.join(out_dir, "markdown")
            if os.path.isdir(md_dir):
                md_files = list(Path(md_dir).glob("*.md"))
                if md_files:
                    st.subheader("生成されたMarkdown")
                    for md_file in md_files:
                        st.markdown(f"### {md_file.name}")
                        try:
                            text = md_file.read_text(encoding="utf-8")
                            st.markdown(text)
                        except Exception:
                            st.write(f"{md_file.name} の読み込みに失敗しました。")

            # Show figures if present
            fig_dir = os.path.join(out_dir, "markdown", "figures")
            if os.path.isdir(fig_dir):
                figs = list(Path(fig_dir).iterdir())
                if figs:
                    st.subheader("抽出された図")
                    cols = st.columns(3)
                    for i, fig in enumerate(figs):
                        try:
                            cols[i % 3].image(str(fig), caption=fig.name)
                        except Exception:
                            cols[i % 3].write(fig.name)

            # Provide zip download
            archive = zip_output_dir(out_dir)
            if archive and os.path.exists(archive):
                with open(archive, "rb") as f:
                    st.download_button(label="結果をZIPでダウンロード", data=f, file_name=os.path.basename(archive))

        except Exception as e:
            st.error(f"処理中にエラーが発生しました: {str(e)}")


if __name__ == "__main__":
    main()

スクリーンショット

実践: ローカルで動かす手順(ハンズオン)

  1. 仮想環境を用意し、依存をインストールする(上記コマンド参照)。
  2. 環境変数や config/Dolphin.yaml のパスを確認する。
  3. Streamlit アプリを起動する:
Bash
streamlit run streamlit_app.py
  1. ブラウザで表示されるUIから画像またはPDFをアップロードし、「処理開始」を押します。

処理の流れ(内部):

streamlit_app.py のポイント解説(サンプル抜粋)

モデルの読み込み(キャッシュ):

Python
@st.cache_resource
def load_model(config_path: str):
    cfg = OmegaConf.load(config_path)
    model = DOLPHIN(cfg)
    return model

この関数は初回の重いロードを1回だけ行い、以降の操作でモデルを共有します。

解析の呼び出し例(簡潔化):

Python
out_dir = os.path.join(tmp_dir, "outputs")
setup_output_dirs(out_dir)
json_path, results = process_document(tmp_path, model, out_dir, max_batch_size)

処理結果の表示:

コード変更の注意点

Streamlit はバージョンによりパラメータ名が変わる場合があります。以前のサンプルで use_column_width を使っていると警告が出るため、use_container_width=True に置換してください。

実践的な改良案

以下は実運用で有効な改善案です。

よくある問題と対処法

参考リンク

最後まで読んでいただきありがとうございます。
ご意見、ご感想、ご質問は是非コメント欄へお願いします。

モバイルバージョンを終了