-
Notifications
You must be signed in to change notification settings - Fork 0
Collapse file tree
Files
Search this repository
/
Copy pathai-parse-document-debug.py
More file actions
More file actions
Latest commit
1097 lines (934 loc) · 46.4 KB
/
ai-parse-document-debug.py
File metadata and controls
1097 lines (934 loc) · 46.4 KB
You must be signed in to make or propose changes
More edit options
Edit and raw actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
# Databricks notebook source
# MAGIC %md
# MAGIC # 🔍 AI Document Analysis Debug Interface
# MAGIC
# MAGIC バージョン 1.2
# MAGIC
# MAGIC 最終更新日: 2025年10月6日(木)
# MAGIC
# MAGIC 変更履歴:
# MAGIC - `input_file` が未設定の場合、入力ディレクトリ内の全ファイルを処理します。
# MAGIC
# MAGIC ## 概要
# MAGIC このノートブックは、Databricksの `ai_parse_document` 関数の出力を分析する**ビジュアルデバッグインターフェース**を提供します。解析されたドキュメントをインタラクティブなバウンディングボックス付きで表示し、各領域から抽出された内容を確認できます。
# MAGIC
# MAGIC ## 特長
# MAGIC - 📊 **ビジュアルバウンディングボックス**: テキストや要素が検出された領域を色分けして表示
# MAGIC - 🎯 **インタラクティブツールチップ**: バウンディングボックスにカーソルを合わせると、その領域の解析内容を表示
# MAGIC - 📐 **自動スケーリング**: 大きなドキュメントも最適な幅(1024px)に自動調整
# MAGIC - 🎨 **要素タイプの可視化**: テキスト、ヘッダー、表、図など要素ごとに色分け
# MAGIC
# MAGIC ## 必要なパラメータ
# MAGIC
# MAGIC このインターフェースを利用するには、2つのUnity Catalog(UC)ボリュームパスの設定が必要です。
# MAGIC
# MAGIC ### 1. `source_files`
# MAGIC - **説明**: 解析・可視化したいドキュメントが格納されているUCボリュームのパス
# MAGIC - **例**: `/Volumes/catalog/schema/volume/documents/`
# MAGIC - **要件**: PDFや画像ファイルが格納されたボリュームへの読み取り権限
# MAGIC
# MAGIC ### 2. `image_output_path`
# MAGIC - **説明**: `ai_parse_document` が抽出したページ画像を保存する書き込み可能なUCボリュームのパス
# MAGIC - **例**: `/Volumes/catalog/schema/volume/parsed_images/`
# MAGIC - **要件**: 中間画像出力の保存のため書き込み権限が必要
# MAGIC - **備考**: [公式Databricksドキュメント](https://docs.databricks.com/aws/en/sql/language-manual/functions/ai_parse_document)に記載の通り、このパスは解析関数がページ画像を保存するために使用されます
# MAGIC
# MAGIC ### 3. `page_selection`
# MAGIC
# MAGIC ページ選択文字列を解析し、表示するページインデックスのリストを返します。対応フォーマット:
# MAGIC - "all" または None: 全ページを表示
# MAGIC - "3": 特定ページ(1始まり)
# MAGIC - "1-5": ページ範囲(両端含む、1始まり)
# MAGIC - "1,3,5": 特定ページのリスト(1始まり)
# MAGIC - "1-3,7,10-12": 範囲と個別ページの混在
# MAGIC
# MAGIC ## 利用手順
# MAGIC
# MAGIC 1. **このノートブックをクローン**してください:
# MAGIC - 上部ツールバーの **「ファイル → クローン」** を選択
# MAGIC - 任意のワークスペースに保存
# MAGIC - これにより、編集・実行可能な個人用コピーが作成されます
# MAGIC
# MAGIC 2. ボリューム内に **input** と **output** ディレクトリを作成し、PDFファイルをinputディレクトリにアップロードしてください。
# MAGIC
# MAGIC 3. ノートブック上部の入力ボックスで**パラメータを設定**してください
# MAGIC
# MAGIC 4. **全てのコードセルを実行**し、ビジュアルデバッグ結果を生成してください
# MAGIC
# MAGIC ## 期待される結果
# MAGIC
# MAGIC - **ドキュメント概要**: ページ数、要素数、メタデータの表示
# MAGIC - **色分け凡例**: 要素タイプごとの色ガイド
# MAGIC - **注釈付き画像**: 各ページにバウンディングボックスを重ねて表示
# MAGIC - 任意のボックスにカーソルを合わせると抽出内容を表示
# MAGIC - 現在ホバー中の要素は黄色で強調
# MAGIC - **解析要素リスト**: 抽出された全要素とその内容の一覧
# COMMAND ----------
# Execution Parameters
dbutils.widgets.text("catalog", "")
dbutils.widgets.text("schema", "")
dbutils.widgets.text("volume", "")
dbutils.widgets.text("input_file", "")
dbutils.widgets.text("page_selection", "all")
catalog = dbutils.widgets.get("catalog")
schema = dbutils.widgets.get("schema")
volume = dbutils.widgets.get("volume")
input_file = dbutils.widgets.get("input_file")
page_selection = dbutils.widgets.get("page_selection")
# COMMAND ----------
# DBTITLE 1,パラメーターの設定
# 設定パラメータ
source_files = f"/Volumes/{catalog}/{schema}/{volume}/input/{input_file}"
image_output_path = f"/Volumes/{catalog}/{schema}/{volume}/output/"
# ページ選択文字列を解析し、表示するページインデックスのリストを返します。
# 対応フォーマット:
# - "all" または None: 全ページを表示
# - "3": 特定ページ(1始まり)
# - "1-5": ページ範囲(両端含む、1始まり)
# - "1,3,5": 特定ページのリスト(1始まり)
# - "1-3,7,10-12": 範囲と個別ページの混在
page_selection = f"{page_selection}"
# COMMAND ----------
# DBTITLE 1,ドキュメントパースコードの実行 (少し時間かかります)
# ドキュメント解析実行コード(時間がかかる場合があります)
import json
# ai_parse_document() を使ったSQL文
if not input_file:
source_files = f"/Volumes/{catalog}/{schema}/{volume}/input/*"
sql = f'''
with parsed_documents AS (
SELECT
path,
ai_parse_document(content
,
map(
'version', '2.0',
'imageOutputPath', '{image_output_path}',
'descriptionElementTypes', '*'
)
) as parsed
FROM
read_files('{source_files}', format => 'binaryFile')
)
select path, to_json(parsed) as parsed_json from parsed_documents
'''
parsed_results = [json.loads(row.parsed_json) for row in spark.sql(sql).collect()]
# COMMAND ----------
# DBTITLE 1,デバッガー関数のロード
# デバッガ関数の読み込み
import base64
import io
import json
import os
from typing import Any, Dict, List, Optional, Set, Tuple, Union
from IPython.display import HTML, display
from PIL import Image
class DocumentRenderer:
def __init__(self):
# 異なる要素タイプの色のマッピング
self.element_colors = {
"section_header": "#FF6B6B",
"text": "#4ECDC4",
"figure": "#45B7D1",
"caption": "#96CEB4",
"page_footer": "#FFEAA7",
"page_header": "#DDA0DD",
"table": "#98D8C8",
"list": "#F7DC6F",
"default": "#BDC3C7",
}
def _parse_page_selection(
self, page_selection: Union[str, None], total_pages: int
) -> Set[int]:
"""ページ選択文字列を解析し、表示するページインデックスのセットを返します。
引数:
page_selection: 選択文字列またはNone
total_pages: 利用可能なページの総数
戻り値:
表示する0ベースのページインデックスのセット
"""
# Noneまたは"all"を処理 - すべてのページを返す
if page_selection is None or page_selection.lower() == "all":
return set(range(total_pages))
selected_pages = set()
# 入力をクリーンアップ
page_selection = page_selection.strip()
# 複数選択のためにカンマで分割
parts = page_selection.split(",")
for part in parts:
part = part.strip()
# 範囲かどうかを確認(ハイフンを含む)
if "-" in part:
try:
# 範囲を分割し、整数に変換
range_parts = part.split("-")
if len(range_parts) == 2:
start = int(range_parts[0].strip())
end = int(range_parts[1].strip())
# 1ベースから0ベースに変換
start_idx = start - 1
end_idx = end - 1
# 範囲内のすべてのページを追加(含む)
for i in range(start_idx, end_idx + 1):
if 0 <= i < total_pages:
selected_pages.add(i)
except ValueError:
print(f"警告: ページ選択の範囲 '{part}' が無効です")
else:
# 単一ページ番号
try:
page_num = int(part.strip())
# 1ベースから0ベースに変換
page_idx = page_num - 1
if 0 <= page_idx < total_pages:
selected_pages.add(page_idx)
else:
print(
f"警告: ページ {page_num} は範囲外です (1-{total_pages})"
)
except ValueError:
print(f"警告: ページ選択の番号 '{part}' が無効です")
# 有効なページが選択されていない場合、すべてのページにデフォルト
if not selected_pages:
print(
f"警告: 選択 '{page_selection}' に有効なページがありません。すべてのページを表示します。"
)
return set(range(total_pages))
return selected_pages
def _get_element_color(self, element_type: str) -> str:
"""要素タイプの色を取得します。"""
return self.element_colors.get(
element_type.lower(), self.element_colors["default"]
)
def _get_image_dimensions(self, image_path: str) -> Optional[Tuple[int, int]]:
"""画像ファイルの寸法を取得します。"""
try:
if os.path.exists(image_path):
with Image.open(image_path) as img:
return img.size # (幅、高さ)を返します
return None
except Exception as e:
print(f"{image_path} の画像寸法を取得中にエラーが発生しました: {e}")
return None
def _load_image_as_base64(self, image_path: str) -> Optional[str]:
"""ファイルパスから画像を読み込み、base64に変換します。"""
try:
if os.path.exists(image_path):
with open(image_path, "rb") as img_file:
img_data = img_file.read()
img_base64 = base64.b64encode(img_data).decode("utf-8")
ext = os.path.splitext(image_path)[1].lower()
if ext in [".jpg", ".jpeg"]:
return f"data:image/jpeg;base64,{img_base64}"
elif ext in [".png"]:
return f"data:image/png;base64,{img_base64}"
else:
return f"data:image/jpeg;base64,{img_base64}"
return None
except Exception as e:
print(f"{image_path} の画像を読み込む中にエラーが発生しました: {e}")
return None
def _render_element_content(self, element: Dict, for_tooltip: bool = False) -> str:
"""ツールチップと要素リスト表示のために適切なフォーマットで要素コンテンツをレンダリングします。
引数:
element: コンテンツ/説明を含む要素辞書
for_tooltip: これはツールチップ表示のためか(スタイリングと切り詰めに影響)
"""
element_type = element.get("type", "unknown")
content = element.get("content", "")
description = element.get("description", "")
display_content = ""
if content:
if element_type == "table":
# スタイリングを適用したHTMLテーブルをレンダリング
table_html = content
# コンテキストに基づいて異なるスタイリングを適用
if for_tooltip:
# ツールチップ用のコンパクトスタイリング
# ツールチップテーブルのために利用可能な全幅を使用
table_style = f'''style="width: 100%; border-collapse: collapse; margin: 5px 0; font-size: 10px;"'''
th_style = 'style="border: 1px solid #ddd; padding: 4px; background: #f8f9fa; color: #333; font-weight: bold; text-align: left; font-size: 10px;"'
td_style = 'style="border: 1px solid #ddd; padding: 4px; color: #333; font-size: 10px;"'
thead_style = 'style="background: #e9ecef;"'
else:
# 要素リスト用のフルスタイリング
table_style = '''style="width: 100%; border-collapse: collapse; margin: 10px 0; font-size: 13px;"'''
th_style = 'style="border: 1px solid #ddd; padding: 8px; background: #f5f5f5; font-weight: bold; text-align: left;"'
# 新しいドキュメントのページを取得
_, pages = get_current_document()
# 新しいドキュメントのためにページコントロールを更新
update_page_controls(pages)
# ドキュメントラベルを更新
if has_multiple_docs:
doc_idx_position = next(i for i, (idx, _) in enumerate(successful_docs) if idx == change["new"])
doc_label.value = f"{doc_idx_position + 1} of {len(successful_docs)} ドキュメント"
# 新しいドキュメントの最初のページをレンダリング
update_page(1)
# イベントハンドラを接続
prev_button.on_click(on_prev_click)
next_button.on_click(on_next_click)
page_slider.observe(on_slider_change, names="value")
page_dropdown.observe(on_page_dropdown_change, names="value")
if has_multiple_docs:
doc_dropdown.observe(on_doc_dropdown_change, names="value")
# レイアウト
if has_multiple_docs:
# ドキュメント行: [ドキュメントドロップダウン] [ラベル]
doc_row = widgets.HBox(
[
doc_dropdown,
doc_label,
],
layout=widgets.Layout(margin="0 0 10px 0")
)
# ページナビゲーション行: [前へ] [スライダー] [次] | [ドロップダウン] [ラベル]
page_nav_row = widgets.HBox(
[
prev_button,
page_slider,
next_button,
widgets.Label(value=" "), # スペーサー
page_dropdown,
page_label,
]
)
# ドキュメントセレクタの上にウィジェットを表示
display(widgets.VBox([doc_row, page_nav_row, output_area]))
else:
# ページナビゲーションのみ: [前へ] [スライダー] [次] | [ドロップダウン] [ラベル]
nav_row = widgets.HBox(
[
prev_button,
page_slider,
next_button,
widgets.Label(value=" "), # スペーサー
page_dropdown,
page_label,
]
)
# ウィジェットを表示
display(widgets.VBox([nav_row, output_area]))
# 初期レンダリングをトリガー
update_page(1)
# COMMAND ----------
# DBTITLE 1,デバッグの可視化結果
# デバッグ可視化結果
render_ai_parse_output_interactive(parsed_results)