amenbo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/adapters/index.d.ts +22 -0
- package/dist/adapters/index.js +51 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/types.d.ts +21 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/cache.d.ts +90 -0
- package/dist/cache.js +286 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts +46 -0
- package/dist/cli.js +327 -0
- package/dist/cli.js.map +1 -0
- package/dist/core.d.ts +78 -0
- package/dist/core.js +649 -0
- package/dist/core.js.map +1 -0
- package/dist/diff.d.ts +20 -0
- package/dist/diff.js +44 -0
- package/dist/diff.js.map +1 -0
- package/dist/errors.d.ts +97 -0
- package/dist/errors.js +209 -0
- package/dist/errors.js.map +1 -0
- package/dist/extract/dataSources.d.ts +10 -0
- package/dist/extract/dataSources.js +115 -0
- package/dist/extract/dataSources.js.map +1 -0
- package/dist/extract/geometry.d.ts +63 -0
- package/dist/extract/geometry.js +77 -0
- package/dist/extract/geometry.js.map +1 -0
- package/dist/extract/markdown.d.ts +24 -0
- package/dist/extract/markdown.js +221 -0
- package/dist/extract/markdown.js.map +1 -0
- package/dist/extract/outline.d.ts +33 -0
- package/dist/extract/outline.js +128 -0
- package/dist/extract/outline.js.map +1 -0
- package/dist/extract/pdf.d.ts +22 -0
- package/dist/extract/pdf.js +95 -0
- package/dist/extract/pdf.js.map +1 -0
- package/dist/extract/preview.d.ts +19 -0
- package/dist/extract/preview.js +94 -0
- package/dist/extract/preview.js.map +1 -0
- package/dist/extract/pruning.d.ts +61 -0
- package/dist/extract/pruning.js +101 -0
- package/dist/extract/pruning.js.map +1 -0
- package/dist/extract/qualityScore.d.ts +67 -0
- package/dist/extract/qualityScore.js +98 -0
- package/dist/extract/qualityScore.js.map +1 -0
- package/dist/fetcher/browser.d.ts +85 -0
- package/dist/fetcher/browser.js +343 -0
- package/dist/fetcher/browser.js.map +1 -0
- package/dist/fetcher/http.d.ts +105 -0
- package/dist/fetcher/http.js +538 -0
- package/dist/fetcher/http.js.map +1 -0
- package/dist/fetcher/index.d.ts +41 -0
- package/dist/fetcher/index.js +92 -0
- package/dist/fetcher/index.js.map +1 -0
- package/dist/jp/consentBanner.d.ts +27 -0
- package/dist/jp/consentBanner.js +48 -0
- package/dist/jp/consentBanner.js.map +1 -0
- package/dist/jp/normalize.d.ts +30 -0
- package/dist/jp/normalize.js +65 -0
- package/dist/jp/normalize.js.map +1 -0
- package/dist/links.d.ts +20 -0
- package/dist/links.js +223 -0
- package/dist/links.js.map +1 -0
- package/dist/politeness.d.ts +66 -0
- package/dist/politeness.js +170 -0
- package/dist/politeness.js.map +1 -0
- package/dist/screenshot.d.ts +38 -0
- package/dist/screenshot.js +90 -0
- package/dist/screenshot.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +111 -0
- package/dist/server.js.map +1 -0
- package/dist/templateLearning.d.ts +11 -0
- package/dist/templateLearning.js +42 -0
- package/dist/templateLearning.js.map +1 -0
- package/dist/tokens.d.ts +47 -0
- package/dist/tokens.js +141 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extract/geometry.ts — Phase 4 ジオメトリ抽出(構造化が甘いdiv soup/テーブルレイアウト対策)。
|
|
3
|
+
*
|
|
4
|
+
* Readability+J4スコアの抽出結果が貧弱(本文が短い)かつJ7アダプタが非該当のページに対する
|
|
5
|
+
* 最終フォールバック。ブラウザ昇格時に採取したテキストブロック(要素単位。真の意味での
|
|
6
|
+
* テキストノード粒度ではなく、p/li/td等の「テキストを直接持つリーフ要素」単位で近似する)の
|
|
7
|
+
* bounding boxから、縦方向に連続し水平位置(左端)が揃うブロック群を1つの「カラム」として
|
|
8
|
+
* クラスタリングし、最もテキスト量の多いクラスタを主コンテンツ領域とみなす。
|
|
9
|
+
*
|
|
10
|
+
* 「セマンティックなDOMではなくレンダリング結果の見た目(ジオメトリ)を一次情報にする」
|
|
11
|
+
* というplan.mdの前提認識(§0)を素朴な形で実装したもの。
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* 表/canvas/svgのbounding box面積の合計 / ページ全体面積(0-1)。
|
|
15
|
+
* Phase 2まではDOM要素数ベースの近似だった「視覚要素占有率」を、実ジオメトリで置き換える。
|
|
16
|
+
* 要素同士の重なりは考慮しない単純合計(重なりは稀なため許容する近似)。
|
|
17
|
+
*/
|
|
18
|
+
export function computeVisualAreaRatio(visualElements, pageWidth, pageHeight) {
|
|
19
|
+
const pageArea = pageWidth * pageHeight;
|
|
20
|
+
if (pageArea <= 0)
|
|
21
|
+
return 0;
|
|
22
|
+
const visualArea = visualElements.reduce((sum, el) => sum + el.width * el.height, 0);
|
|
23
|
+
return Math.min(visualArea / pageArea, 1);
|
|
24
|
+
}
|
|
25
|
+
/** 直前ブロックとの縦方向ギャップが、直前後の平均ブロック高さの何倍まで許容されるか。 */
|
|
26
|
+
const MAX_VERTICAL_GAP_RATIO = 2.5;
|
|
27
|
+
/** 同じカラムとみなす左端座標のズレ許容(px)。 */
|
|
28
|
+
const HORIZONTAL_ALIGN_TOLERANCE_PX = 40;
|
|
29
|
+
function buildCluster(blocks) {
|
|
30
|
+
const totalTextLength = blocks.reduce((sum, b) => sum + b.textLength, 0);
|
|
31
|
+
const left = Math.min(...blocks.map((b) => b.x));
|
|
32
|
+
const right = Math.max(...blocks.map((b) => b.x + b.width));
|
|
33
|
+
const top = Math.min(...blocks.map((b) => b.y));
|
|
34
|
+
const bottom = Math.max(...blocks.map((b) => b.y + b.height));
|
|
35
|
+
return { blockIds: blocks.map((b) => b.id), totalTextLength, top, bottom, left, right };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* テキストブロック群を、縦方向に連続し左端座標が揃うクラスタ(カラム)へ分割する(純関数)。
|
|
39
|
+
* テキスト量の多い順にソートして返す(先頭が最も本文らしいクラスタ)。
|
|
40
|
+
*
|
|
41
|
+
* 実装上の注意: y昇順に処理する際、複数カラム(本文とサイドバー等)が同じy範囲に
|
|
42
|
+
* 交互に出現しうるため、「直前に処理した1ブロックとだけ」比較する単純な線形走査では
|
|
43
|
+
* カラムが混ざってしまう。そのため現在「進行中」の全カラム候補(OpenColumn)を保持し、
|
|
44
|
+
* 各ブロックについて左端座標が近い進行中カラムを探して追加する(見つからなければ新規カラム)。
|
|
45
|
+
*/
|
|
46
|
+
export function clusterTextBlocks(blocks) {
|
|
47
|
+
const sorted = [...blocks]
|
|
48
|
+
.filter((b) => b.textLength > 0 && b.width > 0 && b.height > 0)
|
|
49
|
+
.sort((a, b) => a.y - b.y || a.x - b.x);
|
|
50
|
+
if (sorted.length === 0)
|
|
51
|
+
return [];
|
|
52
|
+
const openColumns = [];
|
|
53
|
+
for (const block of sorted) {
|
|
54
|
+
const target = openColumns.find((column) => {
|
|
55
|
+
const sameColumn = Math.abs(block.x - column.lastX) <= HORIZONTAL_ALIGN_TOLERANCE_PX;
|
|
56
|
+
const verticalGap = block.y - column.lastBottom;
|
|
57
|
+
const avgHeight = (column.lastHeight + block.height) / 2;
|
|
58
|
+
return sameColumn && verticalGap <= Math.max(avgHeight, 1) * MAX_VERTICAL_GAP_RATIO;
|
|
59
|
+
});
|
|
60
|
+
if (target) {
|
|
61
|
+
target.blocks.push(block);
|
|
62
|
+
target.lastX = block.x;
|
|
63
|
+
target.lastBottom = block.y + block.height;
|
|
64
|
+
target.lastHeight = block.height;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
openColumns.push({ blocks: [block], lastX: block.x, lastBottom: block.y + block.height, lastHeight: block.height });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return openColumns.map((column) => buildCluster(column.blocks)).sort((a, b) => b.totalTextLength - a.totalTextLength);
|
|
71
|
+
}
|
|
72
|
+
/** 最もテキスト量が多い(=本文らしい)クラスタを主コンテンツ領域として選ぶ。無ければnull。 */
|
|
73
|
+
export function selectMainContentCluster(blocks) {
|
|
74
|
+
const clusters = clusterTextBlocks(blocks);
|
|
75
|
+
return clusters[0] ?? null;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=geometry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geometry.js","sourceRoot":"","sources":["../../src/extract/geometry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type PageGeometrySnapshot } from "./geometry.js";
|
|
2
|
+
import { type QualityScoreInput } from "./qualityScore.js";
|
|
3
|
+
export interface ExtractOptions {
|
|
4
|
+
url?: string;
|
|
5
|
+
/** CSSセレクタでDOMを絞り込んでから変換する(指定時はアダプタ/Readability/J4 pruning/ジオメトリ抽出をスキップ)。 */
|
|
6
|
+
selector?: string;
|
|
7
|
+
/** Phase 4: ブラウザ昇格時に採取したジオメトリデータ(HTTP tierやhttp未昇格時はnull/未指定)。 */
|
|
8
|
+
geometry?: PageGeometrySnapshot | null;
|
|
9
|
+
}
|
|
10
|
+
/** どの経路で本文候補HTMLを得たか(観測性向上・メタデータ表示用)。 */
|
|
11
|
+
export type ExtractionMethod = "selector" | "adapter" | "readability" | "geometry" | "body-fallback";
|
|
12
|
+
export interface ExtractResult {
|
|
13
|
+
title: string | null;
|
|
14
|
+
markdown: string;
|
|
15
|
+
/** J4 fit-pruningで除去したブロック数(selector指定時は常に0)。 */
|
|
16
|
+
prunedBlockCount: number;
|
|
17
|
+
/** 品質スコア(mode: autoでのスクリーンショット自動切替判定)の入力統計。 */
|
|
18
|
+
qualityInput: QualityScoreInput;
|
|
19
|
+
/** J7サイトアダプタが適用された場合、その識別名(selector指定時・未一致時はnull)。 */
|
|
20
|
+
adapterName: string | null;
|
|
21
|
+
extractionMethod: ExtractionMethod;
|
|
22
|
+
}
|
|
23
|
+
/** HTML文字列をMarkdownへ変換する。 */
|
|
24
|
+
export declare function extractMarkdown(html: string, options?: ExtractOptions): ExtractResult;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extract/markdown.ts — HTML→Markdown変換パイプライン。
|
|
3
|
+
*
|
|
4
|
+
* linkedomでパース → J2 ruby除去 → J8 同意バナー除去 → 品質スコア用統計の採取(元DOM全体)
|
|
5
|
+
* → 本文抽出(優先順: selector指定 > J7サイトアダプタ > Readability > Phase 4ジオメトリ抽出
|
|
6
|
+
* > body全体フォールバック) → turndown(GFM)でMarkdown化 → J3 CJK正規化。
|
|
7
|
+
*
|
|
8
|
+
* 設計判断:
|
|
9
|
+
* - selectorは「抽出前にDOMへ適用」(plan.md)の記述に基づき、アダプタ・Readability・
|
|
10
|
+
* ジオメトリ抽出の全てより優先する。selector指定時はユーザーが本文位置を明示している
|
|
11
|
+
* とみなし、それら全てのヒューリスティックをスキップする(誤って削らないため)。
|
|
12
|
+
* - J7サイトアダプタは自動判定(ユーザー指定ではない)なので、選択した要素の中にも
|
|
13
|
+
* J4 fit-pruningを適用する(アダプタのセレクタがコメント欄等まで含んでしまうケースの保険)。
|
|
14
|
+
* - 品質スコア用の統計(qualityInput)はプルーニング/バナー除去後・selector適用前の
|
|
15
|
+
* ドキュメント全体から採取する。バナーは常にノイズなので統計からは除外し、
|
|
16
|
+
* ページ全体が視覚情報に依存しているかどうかを判定したい。
|
|
17
|
+
* - Phase 4ジオメトリ抽出: Readabilityが本文を特定できない(MIN_READABILITY_TEXT_LENGTH未満)
|
|
18
|
+
* 場合、body全体ダンプへ即座にフォールバックする前に、ブラウザ昇格時のみ利用可能な
|
|
19
|
+
* ジオメトリデータ(bounding box)から主コンテンツ領域を推定する(div soup/テーブル
|
|
20
|
+
* レイアウト対策)。それも失敗した場合のみ最終手段としてbody全体を使う。
|
|
21
|
+
*/
|
|
22
|
+
import { Readability } from "@mozilla/readability";
|
|
23
|
+
import { gfm } from "@joplin/turndown-plugin-gfm";
|
|
24
|
+
import { parseHTML } from "linkedom";
|
|
25
|
+
import TurndownService from "turndown";
|
|
26
|
+
import { findAdapter } from "../adapters/index.js";
|
|
27
|
+
import { ExtractionError } from "../errors.js";
|
|
28
|
+
import { normalizeCjkText, stripRubyAnnotations } from "../jp/normalize.js";
|
|
29
|
+
import { removeConsentBanners } from "../jp/consentBanner.js";
|
|
30
|
+
import { estimateTokens } from "../tokens.js";
|
|
31
|
+
import { computeVisualAreaRatio, selectMainContentCluster } from "./geometry.js";
|
|
32
|
+
import { pruneLowValueBlocks } from "./pruning.js";
|
|
33
|
+
const MIN_READABILITY_TEXT_LENGTH = 200;
|
|
34
|
+
const LEAF_ELEMENT_SELECTOR = "p, li, td, th, span, div, canvas, svg, img, h1, h2, h3, h4, h5, h6, a, blockquote";
|
|
35
|
+
// J6の画像面積比の基準ページ面積(px^2)。実レイアウトを取得できないHTTP tierでも使える
|
|
36
|
+
// 固定近似値として、screenshot.tsの既定タイルサイズ(1280x1080)に揃えている。
|
|
37
|
+
const REFERENCE_PAGE_AREA = 1280 * 1080;
|
|
38
|
+
function createTurndownService() {
|
|
39
|
+
const service = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced" });
|
|
40
|
+
service.use(gfm);
|
|
41
|
+
return service;
|
|
42
|
+
}
|
|
43
|
+
/** J6: img要素の面積占有率(近似)とalt欠落率を集計する。 */
|
|
44
|
+
function collectImageStats(body) {
|
|
45
|
+
const imgs = Array.from(body.querySelectorAll("img"));
|
|
46
|
+
let totalArea = 0;
|
|
47
|
+
let missingAlt = 0;
|
|
48
|
+
for (const img of imgs) {
|
|
49
|
+
const width = Number(img.getAttribute("width")) || 0;
|
|
50
|
+
const height = Number(img.getAttribute("height")) || 0;
|
|
51
|
+
totalArea += width * height;
|
|
52
|
+
if (img.getAttribute("alt") === null)
|
|
53
|
+
missingAlt++;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
imgCount: imgs.length,
|
|
57
|
+
imgMissingAltCount: missingAlt,
|
|
58
|
+
imgAreaRatio: Math.min(totalArea / REFERENCE_PAGE_AREA, 1),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** 品質スコア判定用の統計を、元ドキュメント全体(script/style除く)から採取する。 */
|
|
62
|
+
function collectQualityInput(document, geometry) {
|
|
63
|
+
const body = document.body;
|
|
64
|
+
if (!body) {
|
|
65
|
+
return {
|
|
66
|
+
extractedTextLength: 0,
|
|
67
|
+
visibleTextLength: 0,
|
|
68
|
+
tableCellCount: 0,
|
|
69
|
+
canvasCount: 0,
|
|
70
|
+
svgCount: 0,
|
|
71
|
+
totalLeafElementCount: 0,
|
|
72
|
+
imgCount: 0,
|
|
73
|
+
imgMissingAltCount: 0,
|
|
74
|
+
imgAreaRatio: 0,
|
|
75
|
+
extractedTokenEstimate: 0,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// 元のDOMを変更しないよう、複製上でscript/style/noscriptを取り除いてから可視テキストを測る
|
|
79
|
+
const clone = body.cloneNode(true);
|
|
80
|
+
for (const el of Array.from(clone.querySelectorAll("script, style, noscript"))) {
|
|
81
|
+
el.remove();
|
|
82
|
+
}
|
|
83
|
+
const visibleTextLength = (clone.textContent ?? "").replace(/\s+/g, "").length;
|
|
84
|
+
const tableCellCount = body.querySelectorAll("td, th").length;
|
|
85
|
+
const canvasCount = body.querySelectorAll("canvas").length;
|
|
86
|
+
const svgCount = body.querySelectorAll("svg").length;
|
|
87
|
+
const totalLeafElementCount = body.querySelectorAll(LEAF_ELEMENT_SELECTOR).length;
|
|
88
|
+
const imageStats = collectImageStats(body);
|
|
89
|
+
// Phase 4: ブラウザ昇格時は実ジオメトリから視覚要素占有率を計算し、近似値より優先させる。
|
|
90
|
+
// table要素分の占有率も別途計算し、qualityScore.ts側で表由来分/canvas・svg由来分を
|
|
91
|
+
// 分離できるようにする(表がGFMテーブルとして正しく抽出できているページの誤判定対策)。
|
|
92
|
+
const validGeometry = geometry && geometry.pageWidth > 0 && geometry.pageHeight > 0 ? geometry : null;
|
|
93
|
+
const geometryAreaRatios = validGeometry
|
|
94
|
+
? {
|
|
95
|
+
realVisualAreaRatio: computeVisualAreaRatio(validGeometry.visualElements, validGeometry.pageWidth, validGeometry.pageHeight),
|
|
96
|
+
realTableAreaRatio: computeVisualAreaRatio(validGeometry.visualElements.filter((el) => el.tag === "table"), validGeometry.pageWidth, validGeometry.pageHeight),
|
|
97
|
+
}
|
|
98
|
+
: null;
|
|
99
|
+
return {
|
|
100
|
+
extractedTextLength: 0, // Markdown生成後に上書きする
|
|
101
|
+
visibleTextLength,
|
|
102
|
+
tableCellCount,
|
|
103
|
+
canvasCount,
|
|
104
|
+
svgCount,
|
|
105
|
+
totalLeafElementCount,
|
|
106
|
+
...imageStats,
|
|
107
|
+
extractedTokenEstimate: 0, // Markdown生成後に上書きする
|
|
108
|
+
...(geometryAreaRatios ?? {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** J7: アダプタのcontentSelectors候補を先頭から試し、最初に見つかった要素を返す。 */
|
|
112
|
+
function selectAdapterContent(document, selectors) {
|
|
113
|
+
for (const selector of selectors) {
|
|
114
|
+
const match = document.querySelector(selector);
|
|
115
|
+
if (match)
|
|
116
|
+
return match;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Phase 4: ジオメトリクラスタ(主コンテンツ領域と推定されたテキストブロック群)から、
|
|
122
|
+
* 対応するDOM要素(data-amenbo-gid経由で再選択)のHTMLをクラスタ内の並び順で組み立てる。
|
|
123
|
+
* クラスタの総テキスト量がReadabilityと同じ最低ラインに満たない場合はnullを返す
|
|
124
|
+
* (ジオメトリでも本文らしきものが見つからなかった、という扱い)。
|
|
125
|
+
*/
|
|
126
|
+
function extractByGeometry(document, geometry) {
|
|
127
|
+
const cluster = selectMainContentCluster(geometry.textBlocks);
|
|
128
|
+
if (!cluster || cluster.totalTextLength < MIN_READABILITY_TEXT_LENGTH)
|
|
129
|
+
return null;
|
|
130
|
+
const elementById = new Map();
|
|
131
|
+
for (const el of Array.from(document.querySelectorAll("[data-amenbo-gid]"))) {
|
|
132
|
+
const id = Number(el.getAttribute("data-amenbo-gid"));
|
|
133
|
+
if (!Number.isNaN(id))
|
|
134
|
+
elementById.set(id, el);
|
|
135
|
+
}
|
|
136
|
+
const htmlParts = cluster.blockIds.map((id) => elementById.get(id)?.outerHTML).filter((part) => Boolean(part));
|
|
137
|
+
return htmlParts.length > 0 ? htmlParts.join("\n") : null;
|
|
138
|
+
}
|
|
139
|
+
/** HTML文字列をMarkdownへ変換する。 */
|
|
140
|
+
export function extractMarkdown(html, options = {}) {
|
|
141
|
+
const url = options.url ?? "about:blank";
|
|
142
|
+
const { document } = parseHTML(html);
|
|
143
|
+
stripRubyAnnotations(document);
|
|
144
|
+
removeConsentBanners(document);
|
|
145
|
+
// レンダリング可視テキストに近い統計を、プルーニング/selector適用前の元ドキュメントから採取する
|
|
146
|
+
const qualityInput = collectQualityInput(document, options.geometry);
|
|
147
|
+
let contentHtml;
|
|
148
|
+
let title = document.title || null;
|
|
149
|
+
let prunedBlockCount = 0;
|
|
150
|
+
let adapterName = null;
|
|
151
|
+
let extractionMethod;
|
|
152
|
+
if (options.selector) {
|
|
153
|
+
const matched = Array.from(document.querySelectorAll(options.selector));
|
|
154
|
+
if (matched.length === 0) {
|
|
155
|
+
throw new ExtractionError(url, `selectorに一致する要素がありません: ${options.selector}`);
|
|
156
|
+
}
|
|
157
|
+
contentHtml = matched.map((el) => el.outerHTML).join("\n");
|
|
158
|
+
extractionMethod = "selector";
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const hostname = (() => {
|
|
162
|
+
try {
|
|
163
|
+
return new URL(url).hostname;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return "";
|
|
167
|
+
}
|
|
168
|
+
})();
|
|
169
|
+
const adapter = hostname ? findAdapter(hostname) : null;
|
|
170
|
+
const adapterElement = adapter ? selectAdapterContent(document, adapter.contentSelectors) : null;
|
|
171
|
+
if (adapter && adapterElement) {
|
|
172
|
+
for (const removeSelector of adapter.removeSelectors ?? []) {
|
|
173
|
+
for (const el of Array.from(adapterElement.querySelectorAll(removeSelector))) {
|
|
174
|
+
el.remove();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
prunedBlockCount = pruneLowValueBlocks(adapterElement);
|
|
178
|
+
contentHtml = adapterElement.outerHTML;
|
|
179
|
+
adapterName = adapter.name;
|
|
180
|
+
extractionMethod = "adapter";
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
if (document.body) {
|
|
184
|
+
prunedBlockCount = pruneLowValueBlocks(document.body);
|
|
185
|
+
}
|
|
186
|
+
const article = new Readability(document).parse();
|
|
187
|
+
const geometryHtml = options.geometry ? extractByGeometry(document, options.geometry) : null;
|
|
188
|
+
if (article && article.content && (article.textContent ?? "").trim().length >= MIN_READABILITY_TEXT_LENGTH) {
|
|
189
|
+
contentHtml = article.content;
|
|
190
|
+
title = article.title || title;
|
|
191
|
+
extractionMethod = "readability";
|
|
192
|
+
}
|
|
193
|
+
else if (geometryHtml) {
|
|
194
|
+
// Phase 4: Readabilityが貧弱(かつアダプタ非該当)な場合、body全体ダンプの前に
|
|
195
|
+
// ブラウザ昇格時のジオメトリ(bounding box)から主コンテンツ領域を推定する
|
|
196
|
+
contentHtml = geometryHtml;
|
|
197
|
+
extractionMethod = "geometry";
|
|
198
|
+
}
|
|
199
|
+
else if (document.body) {
|
|
200
|
+
// Readability/ジオメトリいずれも本文を特定できない場合はbody全体にフォールバックする
|
|
201
|
+
contentHtml = document.body.innerHTML;
|
|
202
|
+
extractionMethod = "body-fallback";
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
throw new ExtractionError(url, "本文と判定できる要素がありません");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const turndown = createTurndownService();
|
|
210
|
+
const rawMarkdown = turndown.turndown(contentHtml).trim();
|
|
211
|
+
const markdown = normalizeCjkText(rawMarkdown);
|
|
212
|
+
return {
|
|
213
|
+
title,
|
|
214
|
+
markdown,
|
|
215
|
+
prunedBlockCount,
|
|
216
|
+
qualityInput: { ...qualityInput, extractedTextLength: markdown.length, extractedTokenEstimate: estimateTokens(markdown) },
|
|
217
|
+
adapterName,
|
|
218
|
+
extractionMethod,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=markdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.js","sourceRoot":"","sources":["../../src/extract/markdown.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface OutlineSection {
|
|
2
|
+
id: string;
|
|
3
|
+
level: number;
|
|
4
|
+
heading: string;
|
|
5
|
+
excerpt: string;
|
|
6
|
+
tokens: number;
|
|
7
|
+
}
|
|
8
|
+
export interface OutlineResult {
|
|
9
|
+
sections: OutlineSection[];
|
|
10
|
+
totalTokens: number;
|
|
11
|
+
}
|
|
12
|
+
export interface MarkdownSection {
|
|
13
|
+
id: string;
|
|
14
|
+
level: number;
|
|
15
|
+
heading: string;
|
|
16
|
+
/** 見出し行自身+ネストした子見出しを含む、この節の全内容(Markdown)。fetchツールのsection取得用。 */
|
|
17
|
+
content: string;
|
|
18
|
+
/**
|
|
19
|
+
* 見出し行自身+子見出し(h5/h6等、outlineには現れないもの含む)を挟まない直接の内容のみ。
|
|
20
|
+
* 子節の変更が親節にも「変更あり」として伝播してしまうのを避けるため、diff.ts専用に用意する。
|
|
21
|
+
*/
|
|
22
|
+
ownContent: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 見出し行を境界に、ネストを考慮した節データを構築する。
|
|
26
|
+
* 見出しが1つも無いページは、全文を単一のsection "s1" として扱う。
|
|
27
|
+
* diff.ts(§3-3 差分応答)からも再利用される共通処理。
|
|
28
|
+
*/
|
|
29
|
+
export declare function splitSections(markdown: string): MarkdownSection[];
|
|
30
|
+
/** 見出しツリー(h1-h4)+各節冒頭1文+概算トークン数の一覧を作る。 */
|
|
31
|
+
export declare function buildOutline(markdown: string): OutlineResult;
|
|
32
|
+
/** section IDに対応する節のMarkdown本文(ネストした子見出しを含む)を取得する。無ければnull。 */
|
|
33
|
+
export declare function extractSection(markdown: string, sectionId: string): string | null;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extract/outline.ts — outlineモード。
|
|
3
|
+
*
|
|
4
|
+
* 変換済みMarkdownから見出しツリー(h1-h4)を抽出し、各節の冒頭1文+概算トークン数を
|
|
5
|
+
* 返す(数百トークンに収める段階開示)。各節にはsection IDを付与し、
|
|
6
|
+
* fetchツールの `section` パラメータでその節のMarkdown本文のみを取得できるようにする。
|
|
7
|
+
*
|
|
8
|
+
* 設計判断: section IDは見出しテキストのスラッグ化ではなく連番("s1","s2",...)にする。
|
|
9
|
+
* 日本語見出しはローマ字化が一意に定まらずスラッグが不安定になるため、
|
|
10
|
+
* 連番の方が単純・決定的で `section` パラメータとして扱いやすい。
|
|
11
|
+
*/
|
|
12
|
+
import { estimateTokens } from "../tokens.js";
|
|
13
|
+
const MAX_OUTLINE_LEVEL = 4;
|
|
14
|
+
const EXCERPT_MAX_LENGTH = 80;
|
|
15
|
+
const HEADING_PATTERN = /^(#{1,6})\s+(.*)$/;
|
|
16
|
+
/** フェンスコードブロック内は見出しとして扱わないよう考慮しつつ、見出し行を収集する。 */
|
|
17
|
+
function findHeadingLines(lines) {
|
|
18
|
+
const headings = [];
|
|
19
|
+
let inFence = false;
|
|
20
|
+
lines.forEach((line, lineIndex) => {
|
|
21
|
+
if (/^\s*```/.test(line)) {
|
|
22
|
+
inFence = !inFence;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (inFence)
|
|
26
|
+
return;
|
|
27
|
+
const match = HEADING_PATTERN.exec(line);
|
|
28
|
+
if (match?.[1] && match[2] !== undefined) {
|
|
29
|
+
headings.push({ lineIndex, level: match[1].length, text: match[2].trim() });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
return headings;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 見出し行を境界に、ネストを考慮した節データを構築する。
|
|
36
|
+
* 見出しが1つも無いページは、全文を単一のsection "s1" として扱う。
|
|
37
|
+
* diff.ts(§3-3 差分応答)からも再利用される共通処理。
|
|
38
|
+
*/
|
|
39
|
+
export function splitSections(markdown) {
|
|
40
|
+
const lines = markdown.split("\n");
|
|
41
|
+
const headings = findHeadingLines(lines);
|
|
42
|
+
if (headings.length === 0) {
|
|
43
|
+
if (markdown.trim().length === 0)
|
|
44
|
+
return [];
|
|
45
|
+
return [{ id: "s1", level: 1, heading: "(見出しなし)", content: markdown, ownContent: markdown }];
|
|
46
|
+
}
|
|
47
|
+
const stack = [];
|
|
48
|
+
const emitted = [];
|
|
49
|
+
let counter = 0;
|
|
50
|
+
for (let i = 0; i < headings.length; i++) {
|
|
51
|
+
const heading = headings[i];
|
|
52
|
+
if (!heading)
|
|
53
|
+
continue;
|
|
54
|
+
while (stack.length > 0 && (stack[stack.length - 1]?.level ?? 0) >= heading.level) {
|
|
55
|
+
stack.pop();
|
|
56
|
+
}
|
|
57
|
+
counter++;
|
|
58
|
+
const headingLine = lines[heading.lineIndex] ?? `${"#".repeat(heading.level)} ${heading.text}`;
|
|
59
|
+
const section = {
|
|
60
|
+
id: `s${counter}`,
|
|
61
|
+
level: heading.level,
|
|
62
|
+
heading: heading.text,
|
|
63
|
+
lines: [],
|
|
64
|
+
ownLines: [],
|
|
65
|
+
content: "",
|
|
66
|
+
ownContent: "",
|
|
67
|
+
};
|
|
68
|
+
for (const open of stack)
|
|
69
|
+
open.lines.push(headingLine);
|
|
70
|
+
section.lines.push(headingLine);
|
|
71
|
+
stack.push(section);
|
|
72
|
+
if (heading.level <= MAX_OUTLINE_LEVEL)
|
|
73
|
+
emitted.push(section);
|
|
74
|
+
// h5/h6等outlineに現れない深い見出しの内容は、直近のemitted(h1-h4)祖先のownContentへ
|
|
75
|
+
// 折り畳む(独立したdiffエントリを持たない代わりに、変更が検知されない事態を防ぐ)
|
|
76
|
+
const ownOwner = [...stack].reverse().find((open) => open.level <= MAX_OUTLINE_LEVEL) ?? null;
|
|
77
|
+
if (ownOwner)
|
|
78
|
+
ownOwner.ownLines.push(headingLine);
|
|
79
|
+
const contentStart = heading.lineIndex + 1;
|
|
80
|
+
const contentEnd = headings[i + 1]?.lineIndex ?? lines.length;
|
|
81
|
+
const contentLines = lines.slice(contentStart, contentEnd);
|
|
82
|
+
for (const line of contentLines) {
|
|
83
|
+
for (const open of stack)
|
|
84
|
+
open.lines.push(line);
|
|
85
|
+
}
|
|
86
|
+
if (ownOwner)
|
|
87
|
+
ownOwner.ownLines.push(...contentLines);
|
|
88
|
+
}
|
|
89
|
+
return emitted.map((section) => ({
|
|
90
|
+
id: section.id,
|
|
91
|
+
level: section.level,
|
|
92
|
+
heading: section.heading,
|
|
93
|
+
content: section.lines.join("\n"),
|
|
94
|
+
// 末尾の空行の有無は意味的な差ではないため、比較用のownContentはtrimして正規化する
|
|
95
|
+
// (前後の節の増減で「次の見出しの直前の空行の数」が変わるだけの見かけ上の差分を防ぐ)
|
|
96
|
+
ownContent: section.ownLines.join("\n").trim(),
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
/** 節の内容から、見出し行を除いた本文の冒頭1文を抜き出す。 */
|
|
100
|
+
function extractExcerpt(content) {
|
|
101
|
+
const bodyLines = content.split("\n").filter((line) => !HEADING_PATTERN.test(line));
|
|
102
|
+
const body = bodyLines.join("\n").trim();
|
|
103
|
+
if (body.length === 0)
|
|
104
|
+
return "";
|
|
105
|
+
const sentenceMatch = /[^。!?\n]*[。!?]/u.exec(body);
|
|
106
|
+
const raw = sentenceMatch ? sentenceMatch[0] : (body.split("\n").find((l) => l.trim().length > 0) ?? "");
|
|
107
|
+
return raw.trim().slice(0, EXCERPT_MAX_LENGTH);
|
|
108
|
+
}
|
|
109
|
+
/** 見出しツリー(h1-h4)+各節冒頭1文+概算トークン数の一覧を作る。 */
|
|
110
|
+
export function buildOutline(markdown) {
|
|
111
|
+
const sections = splitSections(markdown);
|
|
112
|
+
return {
|
|
113
|
+
sections: sections.map((section) => ({
|
|
114
|
+
id: section.id,
|
|
115
|
+
level: section.level,
|
|
116
|
+
heading: section.heading,
|
|
117
|
+
excerpt: extractExcerpt(section.content),
|
|
118
|
+
tokens: estimateTokens(section.content),
|
|
119
|
+
})),
|
|
120
|
+
totalTokens: estimateTokens(markdown),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** section IDに対応する節のMarkdown本文(ネストした子見出しを含む)を取得する。無ければnull。 */
|
|
124
|
+
export function extractSection(markdown, sectionId) {
|
|
125
|
+
const sections = splitSections(markdown);
|
|
126
|
+
return sections.find((section) => section.id === sectionId)?.content ?? null;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=outline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outline.js","sourceRoot":"","sources":["../../src/extract/outline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const DEFAULT_PDF_MAX_BYTES: number;
|
|
2
|
+
export interface PdfTextResult {
|
|
3
|
+
hasTextLayer: boolean;
|
|
4
|
+
/** ページ毎のプレーンテキスト(hasTextLayer=falseの場合は空配列)。 */
|
|
5
|
+
pages: string[];
|
|
6
|
+
pageCount: number;
|
|
7
|
+
title: string | null;
|
|
8
|
+
}
|
|
9
|
+
/** URLまたはContent-TypeからPDFかどうかを判定する。 */
|
|
10
|
+
export declare function looksLikePdf(url: string, contentType: string | null): boolean;
|
|
11
|
+
/** PDFバイト列がサイズ上限を超えていないか確認する。超過時は型付きエラーを投げる。 */
|
|
12
|
+
export declare function assertPdfSizeWithinLimit(url: string, byteLength: number, maxBytes?: number): void;
|
|
13
|
+
/** PDFバイト列からテキスト層を抽出する。実質的にテキストが無ければhasTextLayer=false。 */
|
|
14
|
+
export declare function extractPdfText(bytes: Uint8Array): Promise<PdfTextResult>;
|
|
15
|
+
/** テキスト抽出結果をページ見出し付きのMarkdown相当プレーンテキストへ整形する。 */
|
|
16
|
+
export declare function markdownFromPdfText(result: PdfTextResult): string;
|
|
17
|
+
export interface PdfPageImage {
|
|
18
|
+
page: number;
|
|
19
|
+
png: Buffer;
|
|
20
|
+
}
|
|
21
|
+
/** テキスト層が無いPDFのフォールバック: 各ページをPNGにラスタライズする(先頭MAX_RENDER_PAGESまで)。 */
|
|
22
|
+
export declare function renderPdfPages(bytes: Uint8Array): Promise<PdfPageImage[]>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extract/pdf.ts — PDF対応(plan.md §6 Phase3 / 官公庁PDF想定)。
|
|
3
|
+
*
|
|
4
|
+
* pdfjs-distでテキスト層を抽出し、実質的にテキストが無ければ(スキャンPDF)
|
|
5
|
+
* 各ページを画像化してタイルとして返す(mode:autoの品質スコア→ピクセル切替と同じ流儀)。
|
|
6
|
+
*
|
|
7
|
+
* 設計判断: Playwright(Chromium)のヘッドレスビルドは内蔵PDFビューアプラグインが
|
|
8
|
+
* 無効化されており、`page.goto(pdfUrl)`は描画されずダウンロード扱いになることを実機検証で
|
|
9
|
+
* 確認した(`<embed type="application/pdf">`経由でも "Couldn't load plugin" となり失敗)。
|
|
10
|
+
* そのためPDFページの画像化にはPlaywrightを使わず、pdfjs-dist(パース・描画)+
|
|
11
|
+
* @napi-rs/canvas(cairo等のシステム依存が無いprebuiltネイティブモジュール。
|
|
12
|
+
* better-sqlite3と同様の配布容易性)を組み合わせる。
|
|
13
|
+
*/
|
|
14
|
+
import { createCanvas } from "@napi-rs/canvas";
|
|
15
|
+
import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
|
|
16
|
+
import { PayloadTooLargeError } from "../errors.js";
|
|
17
|
+
export const DEFAULT_PDF_MAX_BYTES = 20 * 1024 * 1024; // 20MB
|
|
18
|
+
// これ未満(全ページ平均の抽出文字数/ページ)ならテキスト層が実質無い(スキャンPDF)とみなす。
|
|
19
|
+
// 図表ラベル程度の僅かなテキストは残る場合があるため、完全に0でなくとも低いしきい値にする。
|
|
20
|
+
const MIN_TEXT_CHARS_PER_PAGE = 30;
|
|
21
|
+
// 画像フォールバック時、トークン予算保護のため先頭N ページのみラスタライズする
|
|
22
|
+
const MAX_RENDER_PAGES = 10;
|
|
23
|
+
const RENDER_SCALE = 1.5;
|
|
24
|
+
/** URLまたはContent-TypeからPDFかどうかを判定する。 */
|
|
25
|
+
export function looksLikePdf(url, contentType) {
|
|
26
|
+
if (contentType && /application\/pdf/i.test(contentType))
|
|
27
|
+
return true;
|
|
28
|
+
try {
|
|
29
|
+
return /\.pdf(?:[?#]|$)/i.test(new URL(url).pathname);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return /\.pdf(?:[?#]|$)/i.test(url);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** PDFバイト列がサイズ上限を超えていないか確認する。超過時は型付きエラーを投げる。 */
|
|
36
|
+
export function assertPdfSizeWithinLimit(url, byteLength, maxBytes = DEFAULT_PDF_MAX_BYTES) {
|
|
37
|
+
if (byteLength > maxBytes) {
|
|
38
|
+
throw new PayloadTooLargeError(url, byteLength, maxBytes);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** PDFバイト列からテキスト層を抽出する。実質的にテキストが無ければhasTextLayer=false。 */
|
|
42
|
+
export async function extractPdfText(bytes) {
|
|
43
|
+
// verbosity: 0 (ERRORS)にしてpdf.jsの警告ログでstdout/stderrを汚さないようにする
|
|
44
|
+
const loadingTask = getDocument({ data: bytes, useSystemFonts: true, verbosity: 0 });
|
|
45
|
+
try {
|
|
46
|
+
const doc = await loadingTask.promise;
|
|
47
|
+
const pageCount = doc.numPages;
|
|
48
|
+
const pages = [];
|
|
49
|
+
let totalChars = 0;
|
|
50
|
+
for (let i = 1; i <= pageCount; i++) {
|
|
51
|
+
const page = await doc.getPage(i);
|
|
52
|
+
const content = await page.getTextContent();
|
|
53
|
+
const pageText = content.items.map((item) => ("str" in item ? item.str : "")).join("");
|
|
54
|
+
totalChars += pageText.trim().length;
|
|
55
|
+
pages.push(pageText);
|
|
56
|
+
}
|
|
57
|
+
const metadata = await doc.getMetadata().catch(() => null);
|
|
58
|
+
const infoTitle = metadata?.info?.Title ?? "";
|
|
59
|
+
const title = infoTitle.trim() || null;
|
|
60
|
+
const hasTextLayer = pageCount > 0 && totalChars / pageCount >= MIN_TEXT_CHARS_PER_PAGE;
|
|
61
|
+
return { hasTextLayer, pages: hasTextLayer ? pages : [], pageCount, title };
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
await loadingTask.destroy();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** テキスト抽出結果をページ見出し付きのMarkdown相当プレーンテキストへ整形する。 */
|
|
68
|
+
export function markdownFromPdfText(result) {
|
|
69
|
+
return result.pages.map((text, index) => `## ページ ${index + 1}\n\n${text.trim()}`).join("\n\n");
|
|
70
|
+
}
|
|
71
|
+
/** テキスト層が無いPDFのフォールバック: 各ページをPNGにラスタライズする(先頭MAX_RENDER_PAGESまで)。 */
|
|
72
|
+
export async function renderPdfPages(bytes) {
|
|
73
|
+
// verbosity: 0 (ERRORS)にしてpdf.jsの警告ログでstdout/stderrを汚さないようにする
|
|
74
|
+
const loadingTask = getDocument({ data: bytes, useSystemFonts: true, verbosity: 0 });
|
|
75
|
+
try {
|
|
76
|
+
const doc = await loadingTask.promise;
|
|
77
|
+
const pageCount = Math.min(doc.numPages, MAX_RENDER_PAGES);
|
|
78
|
+
const images = [];
|
|
79
|
+
for (let i = 1; i <= pageCount; i++) {
|
|
80
|
+
const page = await doc.getPage(i);
|
|
81
|
+
const viewport = page.getViewport({ scale: RENDER_SCALE });
|
|
82
|
+
const canvas = createCanvas(Math.ceil(viewport.width), Math.ceil(viewport.height));
|
|
83
|
+
const context = canvas.getContext("2d");
|
|
84
|
+
// @napi-rs/canvasのContext2Dは pdf.js が要求するCanvasRenderingContext2D相当のAPIを実装している。
|
|
85
|
+
// canvas: null + canvasContext指定はDOM非依存(Node)環境向けにpdf.js自身が想定する使い方。
|
|
86
|
+
await page.render({ canvas: null, canvasContext: context, viewport }).promise;
|
|
87
|
+
images.push({ page: i, png: canvas.toBuffer("image/png") });
|
|
88
|
+
}
|
|
89
|
+
return images;
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await loadingTask.destroy();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=pdf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pdf.js","sourceRoot":"","sources":["../../src/extract/pdf.ts"],"names":[],"mappings":"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"}
|