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,19 @@
|
|
|
1
|
+
/** プレビュー可能なテキスト系content-typeかどうか(バイナリ系はfalse)。 */
|
|
2
|
+
export declare function isTextLikeContentType(contentType: string | null): boolean;
|
|
3
|
+
export interface HandoffPreviewResult {
|
|
4
|
+
/** プレビュー本文。 */
|
|
5
|
+
body: string;
|
|
6
|
+
/** 補足情報(CSV/TSVの行数注記や、max_tokens予算による打ち切りの注記)。無ければnull。 */
|
|
7
|
+
note: string | null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 非HTMLコンテンツのプレビューを整形する。バイナリ系content-typeの場合はnull
|
|
11
|
+
* (呼び出し側はメタデータのみの応答にする)。
|
|
12
|
+
*
|
|
13
|
+
* @param sourceTruncated ネットワーク層のプレビュー上限(既定256KB)で本文取得自体を
|
|
14
|
+
* 打ち切っていた場合true。表形式(CSV/TSV)は元々「先頭5行」しか見せない前提のため
|
|
15
|
+
* 既存の注記で十分だが、非表形式(text/plain・JSON等)はmax_tokens予算に達していなくても
|
|
16
|
+
* 「取得できたのはファイルの先頭部分のみ」という事実が伝わらないケースがあったため、
|
|
17
|
+
* その場合のみ専用の注記を追加する。
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildHandoffPreview(bytes: Uint8Array, contentType: string | null, maxTokens: number, sourceTruncated: boolean): HandoffPreviewResult | null;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extract/preview.ts — 機能B: 非HTMLコンテンツのハンドオフ応答用プレビュー整形。
|
|
3
|
+
*
|
|
4
|
+
* text/plain・CSV/TSV・JSON・XML等のテキスト系コンテンツはプレビュー本文を、
|
|
5
|
+
* それ以外(zip/xlsx等のバイナリ系)はメタデータのみを返す方針(server.tsの
|
|
6
|
+
* formatHandoffResponseから利用される)。
|
|
7
|
+
*/
|
|
8
|
+
import { decodeHtmlBytes } from "../fetcher/http.js";
|
|
9
|
+
import { estimateTokens } from "../tokens.js";
|
|
10
|
+
/** プレビュー対象とみなすテキスト系content-type(charset等のパラメータは除いて判定)。 */
|
|
11
|
+
const TEXT_CONTENT_TYPE_RE = /^(text\/(plain|csv|tab-separated-values|xml)|application\/(json|xml))$/i;
|
|
12
|
+
const JSON_OR_XML_SUFFIX_RE = /\+(json|xml)$/i;
|
|
13
|
+
function mimeOnly(contentType) {
|
|
14
|
+
return contentType?.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
15
|
+
}
|
|
16
|
+
/** プレビュー可能なテキスト系content-typeかどうか(バイナリ系はfalse)。 */
|
|
17
|
+
export function isTextLikeContentType(contentType) {
|
|
18
|
+
const mime = mimeOnly(contentType);
|
|
19
|
+
if (!mime)
|
|
20
|
+
return false;
|
|
21
|
+
return TEXT_CONTENT_TYPE_RE.test(mime) || JSON_OR_XML_SUFFIX_RE.test(mime);
|
|
22
|
+
}
|
|
23
|
+
function isDelimited(contentType) {
|
|
24
|
+
const mime = mimeOnly(contentType);
|
|
25
|
+
if (mime === "text/csv")
|
|
26
|
+
return "csv";
|
|
27
|
+
if (mime === "text/tab-separated-values")
|
|
28
|
+
return "tsv";
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const CSV_TSV_PREVIEW_ROWS = 5;
|
|
32
|
+
/** テキストを先頭行=ヘッダ、続く最大5行=データ行としてプレビュー整形する(CSV/TSV共通)。 */
|
|
33
|
+
function buildTabularPreview(text) {
|
|
34
|
+
const lines = text.split(/\r\n|\r|\n/);
|
|
35
|
+
// 末尾の空行(ファイル末尾の改行由来)は行として数えない
|
|
36
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
37
|
+
lines.pop();
|
|
38
|
+
}
|
|
39
|
+
const header = lines[0] ?? "";
|
|
40
|
+
const dataLines = lines.slice(1, 1 + CSV_TSV_PREVIEW_ROWS);
|
|
41
|
+
const body = [header, ...dataLines].join("\n");
|
|
42
|
+
const visibleDataRows = Math.max(lines.length - 1, 0);
|
|
43
|
+
const note = `プレビュー範囲内で確認できたデータ行数: ${visibleDataRows}行中先頭${dataLines.length}行を表示(ファイル全体の行数ではありません。取得範囲の制約による部分プレビューです)`;
|
|
44
|
+
return { body, note };
|
|
45
|
+
}
|
|
46
|
+
/** max_tokens予算に収まる最大長までテキストを切り詰める(J5 CJK対応トークン見積りを流用)。 */
|
|
47
|
+
function capTextByTokenBudget(text, maxTokens) {
|
|
48
|
+
if (estimateTokens(text) <= maxTokens) {
|
|
49
|
+
return { text, truncated: false };
|
|
50
|
+
}
|
|
51
|
+
let lo = 0;
|
|
52
|
+
let hi = text.length;
|
|
53
|
+
while (lo < hi) {
|
|
54
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
55
|
+
if (estimateTokens(text.slice(0, mid)) <= maxTokens) {
|
|
56
|
+
lo = mid;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
hi = mid - 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { text: text.slice(0, lo), truncated: true };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 非HTMLコンテンツのプレビューを整形する。バイナリ系content-typeの場合はnull
|
|
66
|
+
* (呼び出し側はメタデータのみの応答にする)。
|
|
67
|
+
*
|
|
68
|
+
* @param sourceTruncated ネットワーク層のプレビュー上限(既定256KB)で本文取得自体を
|
|
69
|
+
* 打ち切っていた場合true。表形式(CSV/TSV)は元々「先頭5行」しか見せない前提のため
|
|
70
|
+
* 既存の注記で十分だが、非表形式(text/plain・JSON等)はmax_tokens予算に達していなくても
|
|
71
|
+
* 「取得できたのはファイルの先頭部分のみ」という事実が伝わらないケースがあったため、
|
|
72
|
+
* その場合のみ専用の注記を追加する。
|
|
73
|
+
*/
|
|
74
|
+
export function buildHandoffPreview(bytes, contentType, maxTokens, sourceTruncated) {
|
|
75
|
+
if (!isTextLikeContentType(contentType))
|
|
76
|
+
return null;
|
|
77
|
+
const decoded = decodeHtmlBytes(bytes, contentType);
|
|
78
|
+
const delimited = isDelimited(contentType);
|
|
79
|
+
if (delimited) {
|
|
80
|
+
return buildTabularPreview(decoded.text);
|
|
81
|
+
}
|
|
82
|
+
const capped = capTextByTokenBudget(decoded.text, maxTokens);
|
|
83
|
+
if (capped.truncated) {
|
|
84
|
+
return { body: capped.text, note: "プレビューはmax_tokens予算に達したため打ち切りました(取得範囲内でのさらなる切り詰めです)" };
|
|
85
|
+
}
|
|
86
|
+
if (sourceTruncated) {
|
|
87
|
+
return {
|
|
88
|
+
body: capped.text,
|
|
89
|
+
note: "取得範囲の上限に達したため、ファイル先頭部分のみのプレビューです(ファイル全体はさらに続きがあります)",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return { body: capped.text, note: null };
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=preview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview.js","sourceRoot":"","sources":["../../src/extract/preview.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extract/pruning.ts — J4 本文スコアラー + fit-pruning。
|
|
3
|
+
*
|
|
4
|
+
* Readabilityの本文判定ヒューリスティック(英語の単語数/カンマ数前提)は、
|
|
5
|
+
* スペース区切りの無い日本語では機能しにくい。代わりに
|
|
6
|
+
* - 句読点密度(、。等。日本語文の「文らしさ」の代替指標)
|
|
7
|
+
* - Unicode文字比率(\p{L}。英字・CJK等を問わない)
|
|
8
|
+
* - リンク密度(ナビ/ランキング/広告枠ほど高くなる)
|
|
9
|
+
* でブロックをスコアリングし、低価値ブロック(ナビ/ランキング/広告枠/フッター相当:
|
|
10
|
+
* リンク密度高・句読点密度低)をMarkdown化前にDOMから除去する。
|
|
11
|
+
*/
|
|
12
|
+
interface BlockTextStats {
|
|
13
|
+
/** ブロック内の全テキスト。 */
|
|
14
|
+
text: string;
|
|
15
|
+
/** ブロック内のリンク(<a>)テキストのみ。 */
|
|
16
|
+
linkText: string;
|
|
17
|
+
}
|
|
18
|
+
export interface BlockScore {
|
|
19
|
+
/** 句読点(、。!?等)がテキストに占める割合(0-1)。 */
|
|
20
|
+
punctuationDensity: number;
|
|
21
|
+
/** Unicode文字(英字・CJK等、\p{L}に一致する文字)がテキストに占める割合(0-1)。 */
|
|
22
|
+
letterRatio: number;
|
|
23
|
+
/** リンクテキストが全体テキストに占める割合(0-1)。 */
|
|
24
|
+
linkDensity: number;
|
|
25
|
+
/** 総合スコア。0以上を本文らしいブロックとみなす。 */
|
|
26
|
+
score: number;
|
|
27
|
+
}
|
|
28
|
+
/** J4: ブロックのテキスト統計からスコアを算出する(純関数)。 */
|
|
29
|
+
export declare function scoreBlock(stats: BlockTextStats): BlockScore;
|
|
30
|
+
export interface PruneOptions {
|
|
31
|
+
/** これ未満の文字数のブロックはスコアリング対象外(除去しない)。既定20文字。 */
|
|
32
|
+
minTextLength?: number;
|
|
33
|
+
/** このスコア未満のブロックを低価値と判定する。既定0。 */
|
|
34
|
+
scoreThreshold?: number;
|
|
35
|
+
}
|
|
36
|
+
/** pruneLowValueBlocksが要求するDOM要素の最小インターフェース(linkedom/ブラウザDOM双方と構造的に互換)。 */
|
|
37
|
+
export interface PruneHostElement {
|
|
38
|
+
tagName: string;
|
|
39
|
+
children: ArrayLike<PruneHostElement>;
|
|
40
|
+
textContent: string;
|
|
41
|
+
remove(): void;
|
|
42
|
+
querySelectorAll(selector: string): ArrayLike<{
|
|
43
|
+
textContent: string;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* DOM上の低価値ブロックを除去する(document.body等をrootに渡す想定)。
|
|
48
|
+
* 常に除去するタグ(nav/aside/footer/header/form)はその場で除去し子孫を再帰評価しない。
|
|
49
|
+
* それ以外は先に子孫を再帰評価してから自身をスコアリングするボトムアップ走査にする
|
|
50
|
+
* (ページ全体が単一のラッパーdivに包まれている実サイト構成では、内部のnav/footer等を
|
|
51
|
+
* 先に除去してからでないとラッパー全体が「リンク密度の高い1ブロック」として誤って
|
|
52
|
+
* 丸ごと刈られてしまうため。トップダウンのままだと本文まで巻き添えで消える回帰があった)。
|
|
53
|
+
*
|
|
54
|
+
* 戻り値は「独立して刈られた部分木の数」。ある要素が最終的に丸ごと除去される場合、
|
|
55
|
+
* その内部で先に個別除去された子孫(nav/footer等)は既に除去済みの部分木に含まれて
|
|
56
|
+
* しまっており、外側の除去と二重にカウントすべきではない。そのため子の内部除去数
|
|
57
|
+
* (descendantPrunedCount)は、その子自身が生き残った場合にのみ合算し、子自身が
|
|
58
|
+
* 除去された場合は破棄して「1(その子1個の除去)」として数える。
|
|
59
|
+
*/
|
|
60
|
+
export declare function pruneLowValueBlocks(root: PruneHostElement, options?: PruneOptions): number;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extract/pruning.ts — J4 本文スコアラー + fit-pruning。
|
|
3
|
+
*
|
|
4
|
+
* Readabilityの本文判定ヒューリスティック(英語の単語数/カンマ数前提)は、
|
|
5
|
+
* スペース区切りの無い日本語では機能しにくい。代わりに
|
|
6
|
+
* - 句読点密度(、。等。日本語文の「文らしさ」の代替指標)
|
|
7
|
+
* - Unicode文字比率(\p{L}。英字・CJK等を問わない)
|
|
8
|
+
* - リンク密度(ナビ/ランキング/広告枠ほど高くなる)
|
|
9
|
+
* でブロックをスコアリングし、低価値ブロック(ナビ/ランキング/広告枠/フッター相当:
|
|
10
|
+
* リンク密度高・句読点密度低)をMarkdown化前にDOMから除去する。
|
|
11
|
+
*/
|
|
12
|
+
const PUNCTUATION_PATTERN = /[、。!?!?.,]/gu;
|
|
13
|
+
// 結合文字(\p{M})も文字として数える(NFD分解形のアクセント記号等で比率が不当に下がらないように)。
|
|
14
|
+
const LETTER_PATTERN = /[\p{L}\p{M}]/gu;
|
|
15
|
+
// スコア係数(設計判断): 句読点密度を最重視(日本語では「文」の存在そのものが本文らしさの
|
|
16
|
+
// 最も強いシグナル)、リンク密度は本文らしさを大きく減点する要因、文字比率は補助的な弱い
|
|
17
|
+
// シグナル(日本語のナビ/フッターも漢字を含み、英語のナビ/フッターも英字を含むため単独では
|
|
18
|
+
// 本文/非本文を判別できない)。従来はCJK文字のみをカウントしていたため、非CJKページの本文が
|
|
19
|
+
// このボーナスを一切得られず、リンクを多く含む英文の本文ブロックが誤って除去されやすかった。
|
|
20
|
+
// \p{L}(Unicode Letter全般)に対象を広げ、言語に依存しない指標にしている。
|
|
21
|
+
const PUNCTUATION_WEIGHT = 40;
|
|
22
|
+
const LETTER_WEIGHT = 1;
|
|
23
|
+
const LINK_WEIGHT = 5;
|
|
24
|
+
/** J4: ブロックのテキスト統計からスコアを算出する(純関数)。 */
|
|
25
|
+
export function scoreBlock(stats) {
|
|
26
|
+
const text = stats.text.trim();
|
|
27
|
+
// 分母はコードポイント数で数える。正規表現matchの分子(コードポイント単位)と単位を揃えないと、
|
|
28
|
+
// 補助面の文字(CJK拡張B等のサロゲートペア)を含むテキストで比率が過小評価される。
|
|
29
|
+
const length = [...text].length;
|
|
30
|
+
if (length === 0) {
|
|
31
|
+
return { punctuationDensity: 0, letterRatio: 0, linkDensity: 0, score: 0 };
|
|
32
|
+
}
|
|
33
|
+
const punctuationCount = (text.match(PUNCTUATION_PATTERN) ?? []).length;
|
|
34
|
+
const punctuationDensity = punctuationCount / length;
|
|
35
|
+
const letterCount = (text.match(LETTER_PATTERN) ?? []).length;
|
|
36
|
+
const letterRatio = letterCount / length;
|
|
37
|
+
const linkTextLength = [...stats.linkText.trim()].length;
|
|
38
|
+
const linkDensity = Math.min(linkTextLength / length, 1);
|
|
39
|
+
const score = punctuationDensity * PUNCTUATION_WEIGHT + letterRatio * LETTER_WEIGHT - linkDensity * LINK_WEIGHT;
|
|
40
|
+
return { punctuationDensity, letterRatio, linkDensity, score };
|
|
41
|
+
}
|
|
42
|
+
const DEFAULT_MIN_TEXT_LENGTH = 20;
|
|
43
|
+
const DEFAULT_SCORE_THRESHOLD = 0;
|
|
44
|
+
/** 常に除去対象とする非本文タグ(セマンティックに明確なナビ/フッター等)。 */
|
|
45
|
+
const ALWAYS_PRUNE_TAGS = new Set(["NAV", "ASIDE", "FOOTER", "HEADER", "FORM"]);
|
|
46
|
+
/** J4スコアで評価する候補タグ(div soup対応: セマンティックタグを持たない場合)。 */
|
|
47
|
+
const SCORE_CANDIDATE_TAGS = new Set(["DIV", "SECTION", "UL", "OL"]);
|
|
48
|
+
function collectLinkText(element) {
|
|
49
|
+
return Array.from(element.querySelectorAll("a"))
|
|
50
|
+
.map((a) => a.textContent)
|
|
51
|
+
.join("");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* DOM上の低価値ブロックを除去する(document.body等をrootに渡す想定)。
|
|
55
|
+
* 常に除去するタグ(nav/aside/footer/header/form)はその場で除去し子孫を再帰評価しない。
|
|
56
|
+
* それ以外は先に子孫を再帰評価してから自身をスコアリングするボトムアップ走査にする
|
|
57
|
+
* (ページ全体が単一のラッパーdivに包まれている実サイト構成では、内部のnav/footer等を
|
|
58
|
+
* 先に除去してからでないとラッパー全体が「リンク密度の高い1ブロック」として誤って
|
|
59
|
+
* 丸ごと刈られてしまうため。トップダウンのままだと本文まで巻き添えで消える回帰があった)。
|
|
60
|
+
*
|
|
61
|
+
* 戻り値は「独立して刈られた部分木の数」。ある要素が最終的に丸ごと除去される場合、
|
|
62
|
+
* その内部で先に個別除去された子孫(nav/footer等)は既に除去済みの部分木に含まれて
|
|
63
|
+
* しまっており、外側の除去と二重にカウントすべきではない。そのため子の内部除去数
|
|
64
|
+
* (descendantPrunedCount)は、その子自身が生き残った場合にのみ合算し、子自身が
|
|
65
|
+
* 除去された場合は破棄して「1(その子1個の除去)」として数える。
|
|
66
|
+
*/
|
|
67
|
+
export function pruneLowValueBlocks(root, options = {}) {
|
|
68
|
+
const minTextLength = options.minTextLength ?? DEFAULT_MIN_TEXT_LENGTH;
|
|
69
|
+
const scoreThreshold = options.scoreThreshold ?? DEFAULT_SCORE_THRESHOLD;
|
|
70
|
+
const walk = (element) => {
|
|
71
|
+
let prunedCount = 0;
|
|
72
|
+
for (const child of Array.from(element.children)) {
|
|
73
|
+
const tag = child.tagName;
|
|
74
|
+
if (ALWAYS_PRUNE_TAGS.has(tag)) {
|
|
75
|
+
child.remove();
|
|
76
|
+
prunedCount++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// 先に子孫を刈ってから自身を評価する(ボトムアップ)。子孫のnav/footer等が
|
|
80
|
+
// 除去された後の「クリーンな」テキスト/リンク密度で自身のスコアを判定するため。
|
|
81
|
+
const descendantPrunedCount = walk(child);
|
|
82
|
+
if (SCORE_CANDIDATE_TAGS.has(tag)) {
|
|
83
|
+
const text = child.textContent ?? "";
|
|
84
|
+
if (text.trim().length >= minTextLength) {
|
|
85
|
+
const stats = { text, linkText: collectLinkText(child) };
|
|
86
|
+
if (scoreBlock(stats).score < scoreThreshold) {
|
|
87
|
+
child.remove();
|
|
88
|
+
// 子自身が丸ごと除去されるため、内部の個別除去数は合算しない(二重カウント回避)。
|
|
89
|
+
prunedCount++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// 子は生き残ったので、その内部で独立して除去されたぶんはそのまま加算する。
|
|
95
|
+
prunedCount += descendantPrunedCount;
|
|
96
|
+
}
|
|
97
|
+
return prunedCount;
|
|
98
|
+
};
|
|
99
|
+
return walk(root);
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=pruning.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pruning.js","sourceRoot":"","sources":["../../src/extract/pruning.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extract/qualityScore.ts — 品質スコアによるMarkdown/ピクセル自動切替の判定。
|
|
3
|
+
*
|
|
4
|
+
* plan.md §3-4「ハイブリッド自動切替」(Phase 2)+ J6画像文字検知(Phase 3)+
|
|
5
|
+
* Phase 4: ブラウザ昇格時は実ジオメトリ(bounding box)から視覚要素占有率を計算する。
|
|
6
|
+
* HTTP tier(静的HTML)は引き続きDOM構造から得られる安価な近似指標を用いる:
|
|
7
|
+
* - 抽出テキスト密度 = 抽出Markdown文字数 / レンダリング可視テキスト文字数
|
|
8
|
+
* - 視覚要素占有率 = (表セル+canvas+svg要素数) / 全「内容を持ちうる」要素数(近似)
|
|
9
|
+
* ブラウザ昇格時は実bounding boxの面積比(realVisualAreaRatio)で置き換える(Phase 4)
|
|
10
|
+
* いずれも表(td/th)由来分とcanvas/svg由来分を分離して評価する。表は抽出品質が
|
|
11
|
+
* 十分ならスクショ切替の理由にしない(=誤判定対策)が、canvas/svgはテキスト代替が
|
|
12
|
+
* 原理的に存在しないため常に評価する。
|
|
13
|
+
* - 抽出テキストの絶対量(extractedTokenEstimate)が十分あれば、密度低下・表由来の
|
|
14
|
+
* 占有率超過のみを理由にしたscreenshot切替は行わない(J6・canvas/svgは対象外)。
|
|
15
|
+
* - J6 画像文字検知 = img面積占有率(width/height属性からの近似) と alt欠落率
|
|
16
|
+
* (「バナー/画像化料金表」等、画像で情報を出しているページの検知)
|
|
17
|
+
* いずれかが閾値を割れば/超えればMarkdown抽出で情報が失われていると判断し、
|
|
18
|
+
* スクリーンショットへの切替を提案する。
|
|
19
|
+
*/
|
|
20
|
+
export interface QualityScoreInput {
|
|
21
|
+
/** extractMarkdown後のMarkdown本文の文字数。 */
|
|
22
|
+
extractedTextLength: number;
|
|
23
|
+
/** レンダリング結果(script/style除く)の可視テキスト文字数。 */
|
|
24
|
+
visibleTextLength: number;
|
|
25
|
+
/** 表セル(td/th)要素数。 */
|
|
26
|
+
tableCellCount: number;
|
|
27
|
+
/** canvas要素数。 */
|
|
28
|
+
canvasCount: number;
|
|
29
|
+
/** svg要素数。 */
|
|
30
|
+
svgCount: number;
|
|
31
|
+
/** 内容を持ちうる要素の総数(視覚要素占有率の分母)。 */
|
|
32
|
+
totalLeafElementCount: number;
|
|
33
|
+
/** img要素数(J6)。 */
|
|
34
|
+
imgCount: number;
|
|
35
|
+
/** alt属性が全く無いimg要素数(J6。alt=""の意図的な装飾指定は除く)。 */
|
|
36
|
+
imgMissingAltCount: number;
|
|
37
|
+
/** img要素のwidth/height属性から見積もった合計面積 / 基準ページ面積(0-1、J6)。 */
|
|
38
|
+
imgAreaRatio: number;
|
|
39
|
+
/**
|
|
40
|
+
* Phase 4: ブラウザ昇格時のみ、実ジオメトリ(bounding box)から計算した
|
|
41
|
+
* 表/canvas/svgの面積占有率(0-1)。指定時はDOM要素数ベースの近似より優先する。
|
|
42
|
+
*/
|
|
43
|
+
realVisualAreaRatio?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Phase 4: realVisualAreaRatioのうちtable要素のみが占める面積比(0-1)。
|
|
46
|
+
* 指定時はrealVisualAreaRatioから表由来分とcanvas/svg由来分を分離し、表由来分にのみ
|
|
47
|
+
* 抽出品質による免除(下記tableExtractedWell相当)を適用できるようにする。
|
|
48
|
+
* 未指定時(realVisualAreaRatioのみ指定)は分離不能として従来通り合算値をそのまま扱う。
|
|
49
|
+
*/
|
|
50
|
+
realTableAreaRatio?: number;
|
|
51
|
+
/**
|
|
52
|
+
* 抽出Markdown本文の概算トークン数(estimateTokens)。密度(比率)が低くても、
|
|
53
|
+
* 抽出テキストの絶対量が既に十分あるページをscreenshotへ誤って切り替えないための判定に使う
|
|
54
|
+
* (例: 官公庁サイト等、可視テキスト全体に対する抽出割合は低くても、抽出できた本文単体で
|
|
55
|
+
* 十分読める量があるケース)。
|
|
56
|
+
*/
|
|
57
|
+
extractedTokenEstimate: number;
|
|
58
|
+
}
|
|
59
|
+
export interface QualityScoreResult {
|
|
60
|
+
density: number;
|
|
61
|
+
visualOccupancyRatio: number;
|
|
62
|
+
imgMissingAltRatio: number;
|
|
63
|
+
lowQuality: boolean;
|
|
64
|
+
reason: string | null;
|
|
65
|
+
}
|
|
66
|
+
/** 品質スコアを評価し、Markdown抽出で情報が失われていないかを判定する。 */
|
|
67
|
+
export declare function evaluateQuality(input: QualityScoreInput): QualityScoreResult;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extract/qualityScore.ts — 品質スコアによるMarkdown/ピクセル自動切替の判定。
|
|
3
|
+
*
|
|
4
|
+
* plan.md §3-4「ハイブリッド自動切替」(Phase 2)+ J6画像文字検知(Phase 3)+
|
|
5
|
+
* Phase 4: ブラウザ昇格時は実ジオメトリ(bounding box)から視覚要素占有率を計算する。
|
|
6
|
+
* HTTP tier(静的HTML)は引き続きDOM構造から得られる安価な近似指標を用いる:
|
|
7
|
+
* - 抽出テキスト密度 = 抽出Markdown文字数 / レンダリング可視テキスト文字数
|
|
8
|
+
* - 視覚要素占有率 = (表セル+canvas+svg要素数) / 全「内容を持ちうる」要素数(近似)
|
|
9
|
+
* ブラウザ昇格時は実bounding boxの面積比(realVisualAreaRatio)で置き換える(Phase 4)
|
|
10
|
+
* いずれも表(td/th)由来分とcanvas/svg由来分を分離して評価する。表は抽出品質が
|
|
11
|
+
* 十分ならスクショ切替の理由にしない(=誤判定対策)が、canvas/svgはテキスト代替が
|
|
12
|
+
* 原理的に存在しないため常に評価する。
|
|
13
|
+
* - 抽出テキストの絶対量(extractedTokenEstimate)が十分あれば、密度低下・表由来の
|
|
14
|
+
* 占有率超過のみを理由にしたscreenshot切替は行わない(J6・canvas/svgは対象外)。
|
|
15
|
+
* - J6 画像文字検知 = img面積占有率(width/height属性からの近似) と alt欠落率
|
|
16
|
+
* (「バナー/画像化料金表」等、画像で情報を出しているページの検知)
|
|
17
|
+
* いずれかが閾値を割れば/超えればMarkdown抽出で情報が失われていると判断し、
|
|
18
|
+
* スクリーンショットへの切替を提案する。
|
|
19
|
+
*/
|
|
20
|
+
// 設計判断(plan.md §3-4に明記の閾値をそのまま採用)
|
|
21
|
+
const DENSITY_THRESHOLD = 0.6;
|
|
22
|
+
const VISUAL_OCCUPANCY_THRESHOLD = 0.3;
|
|
23
|
+
// 表(td/th)由来の占有率を「抽出品質が悪い」と見なさず不問にするための密度の下限。
|
|
24
|
+
// GFMテーブルはパイプ/区切り行の分だけ文字数が可視テキストより膨らむため、表の内容が
|
|
25
|
+
// 取りこぼし無く抽出できていれば density は概ね1以上になる。1未満は可視テキストの
|
|
26
|
+
// 一部しか抽出できていない(=表が画像/レイアウト崩れ等で読み取れていない)ことを示すため
|
|
27
|
+
// 従来通り占有率超過を低品質理由として扱う。
|
|
28
|
+
const TABLE_DENSITY_OK_THRESHOLD = 1;
|
|
29
|
+
// 密度(比率)だけでは「可視テキストの大半が非本文(ナビ/装飾等)で占められるページ」で
|
|
30
|
+
// 抽出テキストの絶対量が十分でも低密度になり得る。抽出Markdownがこの概算トークン数以上
|
|
31
|
+
// あれば、密度低下のみを理由にしたscreenshot切替は行わない(数百tok目安の最小修正)。
|
|
32
|
+
const MIN_EXTRACTED_TOKENS_FOR_DENSITY_EXEMPTION = 500;
|
|
33
|
+
// J6: 「画像で情報を出しているページ」判定。画像面積が大きい"だけ"では正当な写真記事も
|
|
34
|
+
// 引っかかるため、alt欠落率(アクセシブルなテキスト代替が無い=デコード可能な情報が画像に閉じ込め
|
|
35
|
+
// られている可能性が高い)との組み合わせで判定する。
|
|
36
|
+
const IMG_AREA_RATIO_THRESHOLD = 0.3;
|
|
37
|
+
const IMG_MISSING_ALT_RATIO_THRESHOLD = 0.5;
|
|
38
|
+
/** 品質スコアを評価し、Markdown抽出で情報が失われていないかを判定する。 */
|
|
39
|
+
export function evaluateQuality(input) {
|
|
40
|
+
const density = input.visibleTextLength > 0 ? input.extractedTextLength / input.visibleTextLength : 1;
|
|
41
|
+
// Phase 4: ブラウザ昇格時は実ジオメトリの面積比を優先し、無ければDOM要素数ベースの近似を使う
|
|
42
|
+
const visualOccupancyRatio = input.realVisualAreaRatio ??
|
|
43
|
+
(input.totalLeafElementCount > 0 ? (input.tableCellCount + input.canvasCount + input.svgCount) / input.totalLeafElementCount : 0);
|
|
44
|
+
const imgMissingAltRatio = input.imgCount > 0 ? input.imgMissingAltCount / input.imgCount : 0;
|
|
45
|
+
// 密度が低くても、抽出テキストの絶対量が既に十分(500tok目安)ある場合は密度のみを
|
|
46
|
+
// 理由にしたscreenshot切替をしない(表/canvas/svg占有率・J6画像検知は密度に関わらず
|
|
47
|
+
// このあとも評価するため、canvas/svg起因や極端な視覚依存の検出は損なわない)。
|
|
48
|
+
const hasSubstantialExtractedText = input.extractedTokenEstimate >= MIN_EXTRACTED_TOKENS_FOR_DENSITY_EXEMPTION;
|
|
49
|
+
if (density < DENSITY_THRESHOLD && !hasSubstantialExtractedText) {
|
|
50
|
+
return {
|
|
51
|
+
density,
|
|
52
|
+
visualOccupancyRatio,
|
|
53
|
+
imgMissingAltRatio,
|
|
54
|
+
lowQuality: true,
|
|
55
|
+
reason: `抽出テキスト密度が低いです(${density.toFixed(2)} < ${DENSITY_THRESHOLD})`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// 占有率が閾値超過でも、表(td/th)がGFMテーブルとして取りこぼし無く抽出できている
|
|
59
|
+
// (density高、または抽出テキストの絶対量が十分)場合はスクショ切替の理由にしない。
|
|
60
|
+
// 表は本来「画像でしか表現できないページ」の検出が目的であり、正しくMarkdown化
|
|
61
|
+
// できた表まで巻き込むのは誤判定のため。canvas/svgはテキスト代替が原理的に存在
|
|
62
|
+
// しないため、この免除の対象外(密度・抽出量に関わらず従来通り評価する)。
|
|
63
|
+
const tableCellRatio = input.totalLeafElementCount > 0 ? input.tableCellCount / input.totalLeafElementCount : 0;
|
|
64
|
+
const nonTableVisualRatio = input.totalLeafElementCount > 0 ? (input.canvasCount + input.svgCount) / input.totalLeafElementCount : 0;
|
|
65
|
+
// 表由来分とcanvas/svg由来分を分離する。Phase4実ジオメトリはrealTableAreaRatioが
|
|
66
|
+
// 併せて指定されている場合のみ分離可能(未指定時は分離不能として全量を
|
|
67
|
+
// 「canvas/svg相当」= 免除対象外に倒し、従来通り安全側で評価する)。
|
|
68
|
+
const tableRatio = input.realVisualAreaRatio !== undefined ? (input.realTableAreaRatio ?? 0) : tableCellRatio;
|
|
69
|
+
const nonTableRatio = input.realVisualAreaRatio !== undefined
|
|
70
|
+
? Math.max(input.realVisualAreaRatio - (input.realTableAreaRatio ?? 0), 0)
|
|
71
|
+
: nonTableVisualRatio;
|
|
72
|
+
const tableExtractedWell = density >= TABLE_DENSITY_OK_THRESHOLD || hasSubstantialExtractedText;
|
|
73
|
+
const occupancyForLowQualityCheck = tableExtractedWell ? nonTableRatio : tableRatio + nonTableRatio;
|
|
74
|
+
if (occupancyForLowQualityCheck > VISUAL_OCCUPANCY_THRESHOLD) {
|
|
75
|
+
return {
|
|
76
|
+
density,
|
|
77
|
+
visualOccupancyRatio,
|
|
78
|
+
imgMissingAltRatio,
|
|
79
|
+
lowQuality: true,
|
|
80
|
+
// code-reviewer指摘: reasonには判定に実際に使った値(occupancyForLowQualityCheck)を
|
|
81
|
+
// 表示する。tableExtractedWell=true(表は取りこぼし無く抽出済み)の場合はcanvas/svg
|
|
82
|
+
// 由来分のみで判定しているため、旧来の合算値visualOccupancyRatioを表示すると
|
|
83
|
+
// 「表が原因」と誤解させてしまう。
|
|
84
|
+
reason: `表/canvas/svgの要素占有率が高いです(${occupancyForLowQualityCheck.toFixed(2)} > ${VISUAL_OCCUPANCY_THRESHOLD})`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (input.imgAreaRatio > IMG_AREA_RATIO_THRESHOLD && imgMissingAltRatio > IMG_MISSING_ALT_RATIO_THRESHOLD) {
|
|
88
|
+
return {
|
|
89
|
+
density,
|
|
90
|
+
visualOccupancyRatio,
|
|
91
|
+
imgMissingAltRatio,
|
|
92
|
+
lowQuality: true,
|
|
93
|
+
reason: `J6: 画像で情報を提供しているページの可能性があります(画像面積比${input.imgAreaRatio.toFixed(2)} > ${IMG_AREA_RATIO_THRESHOLD}, alt欠落率${imgMissingAltRatio.toFixed(2)} > ${IMG_MISSING_ALT_RATIO_THRESHOLD})`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return { density, visualOccupancyRatio, imgMissingAltRatio, lowQuality: false, reason: null };
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=qualityScore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qualityScore.js","sourceRoot":"","sources":["../../src/extract/qualityScore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fetcher/browser.ts — Playwright chromium共有インスタンス。
|
|
3
|
+
*
|
|
4
|
+
* 二段フェッチ(fetcher/index.ts)でSPAと判定された場合のみ起動する遅延初期化。
|
|
5
|
+
* プロセス終了シグナルでクリーンアップする。
|
|
6
|
+
*/
|
|
7
|
+
import { type Browser, type BrowserContext, type BrowserContextOptions, type Page, type Response as PlaywrightResponse } from "playwright";
|
|
8
|
+
import type { PageGeometrySnapshot } from "../extract/geometry.js";
|
|
9
|
+
export type { PageGeometrySnapshot } from "../extract/geometry.js";
|
|
10
|
+
/** 共有chromiumインスタンスを取得する(未起動なら遅延起動)。 */
|
|
11
|
+
export declare function getBrowser(): Promise<Browser>;
|
|
12
|
+
/** 共有chromium/システムChromeインスタンスを終了する(プロセス終了時・テストのクリーンアップ用)。 */
|
|
13
|
+
export declare function closeBrowser(): Promise<void>;
|
|
14
|
+
export interface BrowserFetchResult {
|
|
15
|
+
finalUrl: string;
|
|
16
|
+
html: string;
|
|
17
|
+
status: number;
|
|
18
|
+
geometry: PageGeometrySnapshot;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Phase 4 ジオメトリ抽出用に、テキストを直接持つリーフ要素の bounding box・フォントサイズ・
|
|
22
|
+
* 可視性と、表/canvas/svgの bounding box を採取する。
|
|
23
|
+
*
|
|
24
|
+
* 対象要素には一時属性 `data-amenbo-gid` を付与する。これは page.content() 呼び出し前に
|
|
25
|
+
* 実行することで、後段の抽出処理(extract/markdown.ts, linkedomでの再パース)が
|
|
26
|
+
* 同じ要素をCSSセレクタで再選択できるようにするため(ブラウザとNode間でDOM参照を
|
|
27
|
+
* 直接受け渡せないので、属性経由で紐付ける)。
|
|
28
|
+
*/
|
|
29
|
+
export declare function collectPageGeometry(page: Page): Promise<PageGeometrySnapshot>;
|
|
30
|
+
/**
|
|
31
|
+
* J8: 国内同意バナー・アプリ誘導インタースティシャルを実レンダリング結果から隠す。
|
|
32
|
+
*
|
|
33
|
+
* jp/consentBanner.tsの静的DOM版と同じ判定パターンだが、page.evaluate内は
|
|
34
|
+
* ブラウザコンテキストで実行されNode側のクロージャを参照できないため、判定パターンを
|
|
35
|
+
* self-containedな関数として定義し直している(多少の重複はやむを得ない設計判断)。
|
|
36
|
+
* 実ブラウザではcomputed styleが取得できるため、position:fixed等の判定も加えている。
|
|
37
|
+
* DOMから除去するのではなく非表示にする(スクリーンショット/SPA判定への影響を抑えつつ
|
|
38
|
+
* 視覚的な妨げだけを取り除く)。
|
|
39
|
+
*/
|
|
40
|
+
export declare function hideConsentBanners(page: Page): Promise<number>;
|
|
41
|
+
/**
|
|
42
|
+
* C1: browser.ts(fetchWithBrowser)・screenshot.ts(captureTiledScreenshot)は
|
|
43
|
+
* fetcher/http.tsのSSRF/スキーム検証(guardPublicAddress)を経由せずpage.goto()していたため、
|
|
44
|
+
* `file:///etc/passwd` や `http://169.254.169.254/...` が素通りしていた。
|
|
45
|
+
*
|
|
46
|
+
* 対応:
|
|
47
|
+
* 1. page.goto()の前にguardPublicAddressで初回遷移先を検証する
|
|
48
|
+
* 2. context.route()で全リクエストを横取りし、メインフレームのナビゲーション
|
|
49
|
+
* リクエスト(初回遷移・各リダイレクトホップの両方が該当する)についてのみ
|
|
50
|
+
* 再度guardPublicAddressを通す。private/予約アドレスやhttp(s)以外のスキームへの
|
|
51
|
+
* 遷移が検出された場合はそのリクエスト自体をabortし(=実接続前に遮断)、
|
|
52
|
+
* 型付きエラー(PrivateAddressError/InvalidUrlError)に変換してgoto()の失敗を報告する
|
|
53
|
+
*
|
|
54
|
+
* サブリソース(img/xhr等)は対象外(スコープはユーザー指定URLへの「遷移」の安全性)。
|
|
55
|
+
* 完全なTOCTOU対策(fetcher/http.tsのssrfSafeLookupのような接続時点での検証)は
|
|
56
|
+
* Playwright側では現実的に組み込めないため、遷移前検証+リダイレクト時の再検証という
|
|
57
|
+
* 「可能な範囲」の対策にとどめる。
|
|
58
|
+
*/
|
|
59
|
+
export declare function navigateSafely(page: Page, url: string, timeoutMs: number): Promise<PlaywrightResponse | null>;
|
|
60
|
+
/**
|
|
61
|
+
* page.goto()の失敗が、ERR_HTTP2_PROTOCOL_ERROR系(実機検証: initial.inc等でPlaywright
|
|
62
|
+
* pinビルドのChromiumのみ発生し、素のcurl --http2やシステムのGoogle Chromeでは成功する
|
|
63
|
+
* ブラウザビルド固有のHTTP/2非互換)かどうかを判定する。この場合のみシステムChromeへの
|
|
64
|
+
* フォールバックを試みる(接続拒否/タイムアウト等の一般的なネットワークエラーは対象外。
|
|
65
|
+
* 常時Chrome起動という追加コストを、明確にビルド固有と分かっているケース以外では払わない)。
|
|
66
|
+
*/
|
|
67
|
+
export declare function isChromiumHttp2NavigationError(error: unknown): boolean;
|
|
68
|
+
export interface BrowserNavigation {
|
|
69
|
+
context: BrowserContext;
|
|
70
|
+
page: Page;
|
|
71
|
+
response: PlaywrightResponse | null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 共有chromiumでcontext/pageを作りnavigateSafelyでナビゲーションする。ERR_HTTP2_PROTOCOL_ERROR系
|
|
75
|
+
* で失敗した場合のみ、システムのGoogle Chromeへ1回だけフォールバックする。
|
|
76
|
+
*
|
|
77
|
+
* フォールバックが行われないケース(元のエラーをそのまま投げ、呼び出し元の既存の
|
|
78
|
+
* エラー分類・メッセージ生成をそのまま活かす):
|
|
79
|
+
* - HTTP2以外のエラー(接続拒否/タイムアウト等)
|
|
80
|
+
* - システムにChromeが無くgetSystemChromeBrowser自体が失敗した場合
|
|
81
|
+
* Chromeでの再試行も失敗した場合は、そちらのエラー(Chrome経由での実際の失敗内容)を投げる。
|
|
82
|
+
*/
|
|
83
|
+
export declare function openPageAndNavigate(url: string, timeoutMs: number, contextOptions?: BrowserContextOptions): Promise<BrowserNavigation>;
|
|
84
|
+
/** Playwrightでページをレンダリングし、レンダリング後のHTMLを取得する。 */
|
|
85
|
+
export declare function fetchWithBrowser(url: string, timeoutMs?: number): Promise<BrowserFetchResult>;
|