@sirdaspdf/core 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 +56 -0
- package/dist/anonymize.d.ts +35 -0
- package/dist/anonymize.js +113 -0
- package/dist/chunk.d.ts +13 -0
- package/dist/chunk.js +93 -0
- package/dist/contamination.d.ts +45 -0
- package/dist/contamination.js +74 -0
- package/dist/dataset.d.ts +69 -0
- package/dist/dataset.js +95 -0
- package/dist/datasplit.d.ts +30 -0
- package/dist/datasplit.js +49 -0
- package/dist/dedupe.d.ts +62 -0
- package/dist/dedupe.js +159 -0
- package/dist/diff.d.ts +44 -0
- package/dist/diff.js +133 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +24 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +21 -0
- package/dist/lossless.d.ts +2 -0
- package/dist/lossless.js +10 -0
- package/dist/markdown.d.ts +21 -0
- package/dist/markdown.js +177 -0
- package/dist/merge.d.ts +1 -0
- package/dist/merge.js +10 -0
- package/dist/ocr.d.ts +24 -0
- package/dist/ocr.js +46 -0
- package/dist/organize.d.ts +5 -0
- package/dist/organize.js +13 -0
- package/dist/paragraphs.d.ts +68 -0
- package/dist/paragraphs.js +138 -0
- package/dist/ranges.d.ts +2 -0
- package/dist/ranges.js +23 -0
- package/dist/split.d.ts +3 -0
- package/dist/split.js +17 -0
- package/dist/tables.d.ts +44 -0
- package/dist/tables.js +186 -0
- package/dist/text.d.ts +19 -0
- package/dist/text.js +74 -0
- package/dist/verifiable.d.ts +60 -0
- package/dist/verifiable.js +244 -0
- package/package.json +72 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Reparto en entrenamiento / validación / prueba.
|
|
2
|
+
//
|
|
3
|
+
// El error que casi todo el mundo comete: trocear los documentos primero y
|
|
4
|
+
// repartir los trozos después. Dos trozos del mismo contrato acaban uno en
|
|
5
|
+
// train y otro en test, el modelo ya vio la plantilla, y la métrica sale
|
|
6
|
+
// preciosa y mentirosa. Aquí el reparto es SIEMPRE por documento: todos los
|
|
7
|
+
// trozos de un documento caen en la misma partición.
|
|
8
|
+
import { fnv1a } from "./text.js";
|
|
9
|
+
const DEFAULT_RATIOS = { train: 0.8, validation: 0.1, test: 0.1 };
|
|
10
|
+
/**
|
|
11
|
+
* Reparte registros agrupándolos por documento de origen.
|
|
12
|
+
*
|
|
13
|
+
* `docOf` dice a qué documento pertenece cada registro. El reparto se decide
|
|
14
|
+
* hasheando el nombre del documento junto con la semilla, así que es estable:
|
|
15
|
+
* volver a ejecutarlo con los mismos archivos da exactamente el mismo reparto,
|
|
16
|
+
* y añadir un documento nuevo no mueve a los demás de partición.
|
|
17
|
+
*/
|
|
18
|
+
export function splitByDocument(records, docOf, options = {}) {
|
|
19
|
+
const ratios = options.ratios ?? DEFAULT_RATIOS;
|
|
20
|
+
const seed = options.seed ?? "sirdas";
|
|
21
|
+
const total = ratios.train + ratios.validation + ratios.test;
|
|
22
|
+
if (total <= 0)
|
|
23
|
+
throw new Error("las proporciones deben sumar más de cero");
|
|
24
|
+
const trainEnd = ratios.train / total;
|
|
25
|
+
const valEnd = (ratios.train + ratios.validation) / total;
|
|
26
|
+
const bucketOf = new Map();
|
|
27
|
+
const decide = (doc) => {
|
|
28
|
+
const cached = bucketOf.get(doc);
|
|
29
|
+
if (cached)
|
|
30
|
+
return cached;
|
|
31
|
+
// El hash en [0,1): reparto determinista y sin estado compartido. El
|
|
32
|
+
// separador evita que "ab"+"c" y "a"+"bc" colisionen.
|
|
33
|
+
const r = fnv1a(`${seed}␟${doc}`) / 0x100000000;
|
|
34
|
+
const bucket = r < trainEnd ? "train" : r < valEnd ? "validation" : "test";
|
|
35
|
+
bucketOf.set(doc, bucket);
|
|
36
|
+
return bucket;
|
|
37
|
+
};
|
|
38
|
+
const out = {
|
|
39
|
+
train: [],
|
|
40
|
+
validation: [],
|
|
41
|
+
test: [],
|
|
42
|
+
documents: { train: 0, validation: 0, test: 0 },
|
|
43
|
+
};
|
|
44
|
+
for (const r of records)
|
|
45
|
+
out[decide(docOf(r))].push(r);
|
|
46
|
+
for (const bucket of bucketOf.values())
|
|
47
|
+
out.documents[bucket]++;
|
|
48
|
+
return out;
|
|
49
|
+
}
|
package/dist/dedupe.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface DedupeOptions {
|
|
2
|
+
/** Palabras por shingle. Más alto = más estricto. */
|
|
3
|
+
shingleSize?: number;
|
|
4
|
+
/** Tamaño de la firma MinHash. Más alto = mejor estimación, más lento. */
|
|
5
|
+
hashes?: number;
|
|
6
|
+
/**
|
|
7
|
+
* Bandas LSH. Debe dividir a `hashes`. Si se omite se deduce del umbral,
|
|
8
|
+
* que es lo que hay que hacer: el punto medio de la curva de LSH es
|
|
9
|
+
* (1/bandas)^(1/filas), y si no acompaña al umbral, bajarlo no sirve de nada
|
|
10
|
+
* porque los pares parecidos ni siquiera llegan a compararse.
|
|
11
|
+
*/
|
|
12
|
+
bands?: number;
|
|
13
|
+
/** Jaccard mínimo para considerar que dos documentos son el mismo. */
|
|
14
|
+
threshold?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface Duplicate {
|
|
17
|
+
/** Índice del documento que se descarta. */
|
|
18
|
+
index: number;
|
|
19
|
+
/** Índice del primero que apareció, y que se conserva. */
|
|
20
|
+
duplicateOf: number;
|
|
21
|
+
similarity: number;
|
|
22
|
+
exact: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface DedupeResult {
|
|
25
|
+
/** Índices que sobreviven, en su orden original. */
|
|
26
|
+
keep: number[];
|
|
27
|
+
duplicates: Duplicate[];
|
|
28
|
+
/** Cuántos eran idénticos una vez normalizados. */
|
|
29
|
+
exact: number;
|
|
30
|
+
/** Cuántos eran parecidos pero no idénticos. */
|
|
31
|
+
near: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Elige el bandeado de LSH para un umbral dado.
|
|
35
|
+
*
|
|
36
|
+
* Se toma el bandeado más barato (menos bandas, menos candidatos que verificar)
|
|
37
|
+
* que aun así proponga el 95 % de los pares que están justo en el umbral. El
|
|
38
|
+
* criterio es la recuperación y no el punto de cruce de la curva, porque el
|
|
39
|
+
* número de firmas solo admite divisores potencia de dos y ajustar por el cruce
|
|
40
|
+
* deja huecos: a umbral 0.9 el bandeado "elegante" recupera el 81 %, es decir,
|
|
41
|
+
* uno de cada cinco duplicados se pierde sin que nadie se entere. En un auditor
|
|
42
|
+
* de datasets un falso negativo silencioso es el peor resultado posible.
|
|
43
|
+
*
|
|
44
|
+
* El precio: con umbrales bajos (0.5 y menos) casi todos los pares acaban
|
|
45
|
+
* siendo candidatos y el coste se acerca al de comparar todo contra todo. Es
|
|
46
|
+
* inherente a pedir "parecido a medias", no un defecto de la implementación.
|
|
47
|
+
*/
|
|
48
|
+
export declare function bandsForThreshold(hashes: number, threshold: number, target?: number): number;
|
|
49
|
+
/**
|
|
50
|
+
* Firma MinHash. Usa una familia de permutaciones (a·x + b mod p) con
|
|
51
|
+
* coeficientes derivados de un generador con semilla fija: el resultado es
|
|
52
|
+
* reproducible, que es requisito para poder auditar un dataset.
|
|
53
|
+
*/
|
|
54
|
+
export declare function minhash(shingles: Set<number>, hashes: number): Uint32Array;
|
|
55
|
+
/** Fracción de posiciones iguales entre dos firmas: estima el Jaccard. */
|
|
56
|
+
export declare function signatureSimilarity(a: Uint32Array, b: Uint32Array): number;
|
|
57
|
+
/**
|
|
58
|
+
* Encuentra los duplicados de un corpus y devuelve qué conservar.
|
|
59
|
+
* Se conserva siempre la primera aparición, para que el resultado no dependa
|
|
60
|
+
* del orden en que el sistema de archivos devolvió los documentos.
|
|
61
|
+
*/
|
|
62
|
+
export declare function dedupe(texts: string[], options?: DedupeOptions): DedupeResult;
|
package/dist/dedupe.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Deduplicación de un corpus por similitud, con MinHash + LSH.
|
|
2
|
+
//
|
|
3
|
+
// Por qué importa: un texto repetido en los datos de entrenamiento se aprende
|
|
4
|
+
// de memoria en vez de generalizarse. Un corpus hecho de PDFs propios está
|
|
5
|
+
// lleno de repeticiones que nadie puso a propósito — la misma plantilla de
|
|
6
|
+
// contrato, el mismo pie de página, el mismo informe en dos versiones.
|
|
7
|
+
//
|
|
8
|
+
// Comparar todos los pares es O(n²) y se vuelve inviable enseguida. MinHash
|
|
9
|
+
// resume cada documento en una firma corta, y LSH agrupa en el mismo cubo a los
|
|
10
|
+
// que probablemente se parecen; solo esos pares se comparan de verdad, y la
|
|
11
|
+
// comparación final es Jaccard exacto sobre los shingles, no la estimación de
|
|
12
|
+
// la firma, para no dar por duplicado algo que no lo es.
|
|
13
|
+
import { fnv1a, ngramHashes, jaccard } from "./text.js";
|
|
14
|
+
const DEFAULTS = { shingleSize: 5, hashes: 128, threshold: 0.8 };
|
|
15
|
+
/** Probabilidad de que LSH proponga como candidato un par con similitud `j`. */
|
|
16
|
+
function lshRecall(bands, rows, j) {
|
|
17
|
+
return 1 - Math.pow(1 - Math.pow(j, rows), bands);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Elige el bandeado de LSH para un umbral dado.
|
|
21
|
+
*
|
|
22
|
+
* Se toma el bandeado más barato (menos bandas, menos candidatos que verificar)
|
|
23
|
+
* que aun así proponga el 95 % de los pares que están justo en el umbral. El
|
|
24
|
+
* criterio es la recuperación y no el punto de cruce de la curva, porque el
|
|
25
|
+
* número de firmas solo admite divisores potencia de dos y ajustar por el cruce
|
|
26
|
+
* deja huecos: a umbral 0.9 el bandeado "elegante" recupera el 81 %, es decir,
|
|
27
|
+
* uno de cada cinco duplicados se pierde sin que nadie se entere. En un auditor
|
|
28
|
+
* de datasets un falso negativo silencioso es el peor resultado posible.
|
|
29
|
+
*
|
|
30
|
+
* El precio: con umbrales bajos (0.5 y menos) casi todos los pares acaban
|
|
31
|
+
* siendo candidatos y el coste se acerca al de comparar todo contra todo. Es
|
|
32
|
+
* inherente a pedir "parecido a medias", no un defecto de la implementación.
|
|
33
|
+
*/
|
|
34
|
+
export function bandsForThreshold(hashes, threshold, target = 0.95) {
|
|
35
|
+
const divisors = [];
|
|
36
|
+
for (let b = 1; b <= hashes; b++)
|
|
37
|
+
if (hashes % b === 0)
|
|
38
|
+
divisors.push(b);
|
|
39
|
+
for (const b of divisors) {
|
|
40
|
+
if (lshRecall(b, hashes / b, threshold) >= target)
|
|
41
|
+
return b;
|
|
42
|
+
}
|
|
43
|
+
// Ningún bandeado llega: el más sensible posible, una fila por banda.
|
|
44
|
+
return hashes;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Firma MinHash. Usa una familia de permutaciones (a·x + b mod p) con
|
|
48
|
+
* coeficientes derivados de un generador con semilla fija: el resultado es
|
|
49
|
+
* reproducible, que es requisito para poder auditar un dataset.
|
|
50
|
+
*/
|
|
51
|
+
export function minhash(shingles, hashes) {
|
|
52
|
+
const P = 4294967291; // primo justo por debajo de 2³²
|
|
53
|
+
const sig = new Uint32Array(hashes).fill(0xffffffff);
|
|
54
|
+
if (shingles.size === 0)
|
|
55
|
+
return sig;
|
|
56
|
+
// Generador congruencial con semilla fija: mismos coeficientes siempre.
|
|
57
|
+
let seed = 0x9e3779b9;
|
|
58
|
+
const next = () => {
|
|
59
|
+
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
|
|
60
|
+
return seed;
|
|
61
|
+
};
|
|
62
|
+
for (let i = 0; i < hashes; i++) {
|
|
63
|
+
const a = (next() % (P - 1)) + 1;
|
|
64
|
+
const b = next() % P;
|
|
65
|
+
let min = 0xffffffff;
|
|
66
|
+
for (const x of shingles) {
|
|
67
|
+
// (a·x + b) mod p sin perder precisión: x < 2³², a < 2³², el producto
|
|
68
|
+
// cabría en 64 bits pero no en un double, así que se parte en dos.
|
|
69
|
+
const hi = Math.floor(x / 65536);
|
|
70
|
+
const lo = x % 65536;
|
|
71
|
+
const v = ((((a * hi) % P) * 65536 + a * lo + b) % P) >>> 0;
|
|
72
|
+
if (v < min)
|
|
73
|
+
min = v;
|
|
74
|
+
}
|
|
75
|
+
sig[i] = min;
|
|
76
|
+
}
|
|
77
|
+
return sig;
|
|
78
|
+
}
|
|
79
|
+
/** Fracción de posiciones iguales entre dos firmas: estima el Jaccard. */
|
|
80
|
+
export function signatureSimilarity(a, b) {
|
|
81
|
+
if (a.length !== b.length || a.length === 0)
|
|
82
|
+
return 0;
|
|
83
|
+
let same = 0;
|
|
84
|
+
for (let i = 0; i < a.length; i++)
|
|
85
|
+
if (a[i] === b[i])
|
|
86
|
+
same++;
|
|
87
|
+
return same / a.length;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Encuentra los duplicados de un corpus y devuelve qué conservar.
|
|
91
|
+
* Se conserva siempre la primera aparición, para que el resultado no dependa
|
|
92
|
+
* del orden en que el sistema de archivos devolvió los documentos.
|
|
93
|
+
*/
|
|
94
|
+
export function dedupe(texts, options = {}) {
|
|
95
|
+
const { shingleSize, hashes, threshold } = { ...DEFAULTS, ...options };
|
|
96
|
+
const bands = options.bands ?? bandsForThreshold(hashes, threshold);
|
|
97
|
+
if (hashes % bands !== 0) {
|
|
98
|
+
throw new Error(`bands (${bands}) debe dividir a hashes (${hashes})`);
|
|
99
|
+
}
|
|
100
|
+
const rows = hashes / bands;
|
|
101
|
+
const shingles = texts.map((t) => ngramHashes(t, shingleSize));
|
|
102
|
+
const sigs = shingles.map((s) => minhash(s, hashes));
|
|
103
|
+
// LSH: dos documentos son candidatos si comparten una banda entera.
|
|
104
|
+
const buckets = new Map();
|
|
105
|
+
for (let i = 0; i < texts.length; i++) {
|
|
106
|
+
for (let b = 0; b < bands; b++) {
|
|
107
|
+
const slice = sigs[i].slice(b * rows, (b + 1) * rows).join(",");
|
|
108
|
+
const key = `${b}:${fnv1a(slice)}`;
|
|
109
|
+
const list = buckets.get(key);
|
|
110
|
+
if (list)
|
|
111
|
+
list.push(i);
|
|
112
|
+
else
|
|
113
|
+
buckets.set(key, [i]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const candidates = new Map();
|
|
117
|
+
for (const list of buckets.values()) {
|
|
118
|
+
if (list.length < 2)
|
|
119
|
+
continue;
|
|
120
|
+
for (let i = 0; i < list.length; i++) {
|
|
121
|
+
for (let j = i + 1; j < list.length; j++) {
|
|
122
|
+
const [lo, hi] = [list[i], list[j]];
|
|
123
|
+
const set = candidates.get(hi) ?? new Set();
|
|
124
|
+
set.add(lo);
|
|
125
|
+
candidates.set(hi, set);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const duplicates = [];
|
|
130
|
+
const dropped = new Set();
|
|
131
|
+
for (let i = 0; i < texts.length; i++) {
|
|
132
|
+
const others = candidates.get(i);
|
|
133
|
+
if (!others)
|
|
134
|
+
continue;
|
|
135
|
+
// Contra el original vivo más temprano: si el candidato ya se descartó,
|
|
136
|
+
// el duplicado debe apuntar al documento que sí se conserva.
|
|
137
|
+
let best = -1;
|
|
138
|
+
let bestSim = 0;
|
|
139
|
+
for (const j of [...others].sort((x, y) => x - y)) {
|
|
140
|
+
if (dropped.has(j))
|
|
141
|
+
continue;
|
|
142
|
+
const sim = jaccard(shingles[i], shingles[j]);
|
|
143
|
+
if (sim >= threshold && sim > bestSim) {
|
|
144
|
+
best = j;
|
|
145
|
+
bestSim = sim;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (best >= 0) {
|
|
149
|
+
dropped.add(i);
|
|
150
|
+
duplicates.push({ index: i, duplicateOf: best, similarity: bestSim, exact: bestSim === 1 });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
keep: texts.map((_, i) => i).filter((i) => !dropped.has(i)),
|
|
155
|
+
duplicates,
|
|
156
|
+
exact: duplicates.filter((d) => d.exact).length,
|
|
157
|
+
near: duplicates.filter((d) => !d.exact).length,
|
|
158
|
+
};
|
|
159
|
+
}
|
package/dist/diff.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { PDFDocumentProxy } from "pdfjs-dist";
|
|
2
|
+
export type ChangeType = "equal" | "added" | "removed";
|
|
3
|
+
export interface DiffLine {
|
|
4
|
+
type: ChangeType;
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
export interface DiffStats {
|
|
8
|
+
added: number;
|
|
9
|
+
removed: number;
|
|
10
|
+
equal: number;
|
|
11
|
+
/** 0 a 1: proporción de líneas que no cambiaron. */
|
|
12
|
+
similarity: number;
|
|
13
|
+
}
|
|
14
|
+
export interface DiffOptions {
|
|
15
|
+
/** Ignora diferencias de espacios y mayúsculas al comparar. */
|
|
16
|
+
ignoreCase?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Tope de seguridad. La comparación exacta cuesta O(n×m); por encima de este
|
|
19
|
+
* producto se devuelve un resultado degradado en vez de colgar el navegador.
|
|
20
|
+
*/
|
|
21
|
+
maxCells?: number;
|
|
22
|
+
}
|
|
23
|
+
/** Divide un texto en líneas comparables, descartando las vacías. */
|
|
24
|
+
export declare function toLines(text: string): string[];
|
|
25
|
+
/** Compara dos listas de líneas y devuelve la secuencia de cambios. */
|
|
26
|
+
export declare function diffLines(a: string[], b: string[], options?: DiffOptions): DiffLine[];
|
|
27
|
+
/**
|
|
28
|
+
* Líneas de un PDF tal y como se ven en la página.
|
|
29
|
+
*
|
|
30
|
+
* Para comparar NO sirve el Markdown: allí los renglones consecutivos se
|
|
31
|
+
* fusionan en párrafos, y entonces cualquier cambio pequeño marca el párrafo
|
|
32
|
+
* entero como distinto. Quien revisa dos versiones de un contrato quiere ver
|
|
33
|
+
* la línea que cambió.
|
|
34
|
+
*/
|
|
35
|
+
export declare function pdfLineTexts(pdf: PDFDocumentProxy, onProgress?: (r: number) => void): Promise<string[]>;
|
|
36
|
+
/** Compara dos textos completos. */
|
|
37
|
+
export declare function diffText(a: string, b: string, options?: DiffOptions): DiffLine[];
|
|
38
|
+
export declare function diffStats(diff: DiffLine[]): DiffStats;
|
|
39
|
+
/** Solo los cambios, con algo de contexto alrededor, como hace `git diff`. */
|
|
40
|
+
export declare function diffChangesOnly(diff: DiffLine[], context?: number): DiffLine[];
|
|
41
|
+
/** Diferencias en Markdown, con el signo delante de cada línea. */
|
|
42
|
+
export declare function diffToMarkdown(diff: DiffLine[]): string;
|
|
43
|
+
/** Tokens aproximados de las líneas cambiadas: útil para pasárselas a una IA. */
|
|
44
|
+
export declare function changedTokens(diff: DiffLine[]): number;
|
package/dist/diff.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { estimateTokens, extractLines } from "./markdown.js";
|
|
2
|
+
const normalize = (s, ignoreCase) => {
|
|
3
|
+
const clean = s.replace(/\s+/g, " ").trim();
|
|
4
|
+
return ignoreCase ? clean.toLowerCase() : clean;
|
|
5
|
+
};
|
|
6
|
+
/** Divide un texto en líneas comparables, descartando las vacías. */
|
|
7
|
+
export function toLines(text) {
|
|
8
|
+
return text
|
|
9
|
+
.split("\n")
|
|
10
|
+
.map((l) => l.trim())
|
|
11
|
+
.filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Subsecuencia común más larga, sobre el tramo que de verdad difiere: antes
|
|
15
|
+
* recortamos el principio y el final idénticos, que en dos versiones de un
|
|
16
|
+
* mismo documento suelen ser casi todo.
|
|
17
|
+
*/
|
|
18
|
+
function lcsDiff(a, b, key) {
|
|
19
|
+
const n = a.length;
|
|
20
|
+
const m = b.length;
|
|
21
|
+
// Tabla de longitudes: table[i][j] = LCS de a[i..] y b[j..].
|
|
22
|
+
const table = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
|
|
23
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
24
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
25
|
+
table[i][j] =
|
|
26
|
+
key(a[i]) === key(b[j])
|
|
27
|
+
? table[i + 1][j + 1] + 1
|
|
28
|
+
: Math.max(table[i + 1][j], table[i][j + 1]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const out = [];
|
|
32
|
+
let i = 0;
|
|
33
|
+
let j = 0;
|
|
34
|
+
while (i < n && j < m) {
|
|
35
|
+
if (key(a[i]) === key(b[j])) {
|
|
36
|
+
out.push({ type: "equal", text: b[j] });
|
|
37
|
+
i++;
|
|
38
|
+
j++;
|
|
39
|
+
}
|
|
40
|
+
else if (table[i + 1][j] >= table[i][j + 1]) {
|
|
41
|
+
out.push({ type: "removed", text: a[i++] });
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
out.push({ type: "added", text: b[j++] });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
while (i < n)
|
|
48
|
+
out.push({ type: "removed", text: a[i++] });
|
|
49
|
+
while (j < m)
|
|
50
|
+
out.push({ type: "added", text: b[j++] });
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/** Compara dos listas de líneas y devuelve la secuencia de cambios. */
|
|
54
|
+
export function diffLines(a, b, options = {}) {
|
|
55
|
+
const ignoreCase = options.ignoreCase ?? false;
|
|
56
|
+
const maxCells = options.maxCells ?? 4_000_000;
|
|
57
|
+
const key = (s) => normalize(s, ignoreCase);
|
|
58
|
+
// Prefijo y sufijo idénticos: se emiten tal cual y se sacan del cálculo.
|
|
59
|
+
let start = 0;
|
|
60
|
+
while (start < a.length && start < b.length && key(a[start]) === key(b[start]))
|
|
61
|
+
start++;
|
|
62
|
+
let endA = a.length;
|
|
63
|
+
let endB = b.length;
|
|
64
|
+
while (endA > start && endB > start && key(a[endA - 1]) === key(b[endB - 1])) {
|
|
65
|
+
endA--;
|
|
66
|
+
endB--;
|
|
67
|
+
}
|
|
68
|
+
const head = a.slice(0, start).map((text) => ({ type: "equal", text }));
|
|
69
|
+
const tail = a.slice(endA).map((text) => ({ type: "equal", text }));
|
|
70
|
+
const midA = a.slice(start, endA);
|
|
71
|
+
const midB = b.slice(start, endB);
|
|
72
|
+
if (!midA.length && !midB.length)
|
|
73
|
+
return [...head, ...tail];
|
|
74
|
+
let middle;
|
|
75
|
+
if (midA.length * midB.length > maxCells) {
|
|
76
|
+
// Documentos enormes y muy distintos: mejor un resultado honesto y rápido
|
|
77
|
+
// que bloquear la pestaña calculando la coincidencia perfecta.
|
|
78
|
+
middle = [
|
|
79
|
+
...midA.map((text) => ({ type: "removed", text })),
|
|
80
|
+
...midB.map((text) => ({ type: "added", text })),
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
middle = lcsDiff(midA, midB, key);
|
|
85
|
+
}
|
|
86
|
+
return [...head, ...middle, ...tail];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Líneas de un PDF tal y como se ven en la página.
|
|
90
|
+
*
|
|
91
|
+
* Para comparar NO sirve el Markdown: allí los renglones consecutivos se
|
|
92
|
+
* fusionan en párrafos, y entonces cualquier cambio pequeño marca el párrafo
|
|
93
|
+
* entero como distinto. Quien revisa dos versiones de un contrato quiere ver
|
|
94
|
+
* la línea que cambió.
|
|
95
|
+
*/
|
|
96
|
+
export async function pdfLineTexts(pdf, onProgress) {
|
|
97
|
+
return (await extractLines(pdf, onProgress)).map((l) => l.text);
|
|
98
|
+
}
|
|
99
|
+
/** Compara dos textos completos. */
|
|
100
|
+
export function diffText(a, b, options = {}) {
|
|
101
|
+
return diffLines(toLines(a), toLines(b), options);
|
|
102
|
+
}
|
|
103
|
+
export function diffStats(diff) {
|
|
104
|
+
const added = diff.filter((d) => d.type === "added").length;
|
|
105
|
+
const removed = diff.filter((d) => d.type === "removed").length;
|
|
106
|
+
const equal = diff.filter((d) => d.type === "equal").length;
|
|
107
|
+
const total = added + removed + equal;
|
|
108
|
+
return { added, removed, equal, similarity: total ? equal / total : 1 };
|
|
109
|
+
}
|
|
110
|
+
/** Solo los cambios, con algo de contexto alrededor, como hace `git diff`. */
|
|
111
|
+
export function diffChangesOnly(diff, context = 2) {
|
|
112
|
+
const keep = new Set();
|
|
113
|
+
diff.forEach((d, i) => {
|
|
114
|
+
if (d.type === "equal")
|
|
115
|
+
return;
|
|
116
|
+
for (let k = Math.max(0, i - context); k <= Math.min(diff.length - 1, i + context); k++)
|
|
117
|
+
keep.add(k);
|
|
118
|
+
});
|
|
119
|
+
return diff.filter((_, i) => keep.has(i));
|
|
120
|
+
}
|
|
121
|
+
/** Diferencias en Markdown, con el signo delante de cada línea. */
|
|
122
|
+
export function diffToMarkdown(diff) {
|
|
123
|
+
return diff
|
|
124
|
+
.map((d) => (d.type === "added" ? `+ ${d.text}` : d.type === "removed" ? `- ${d.text}` : ` ${d.text}`))
|
|
125
|
+
.join("\n");
|
|
126
|
+
}
|
|
127
|
+
/** Tokens aproximados de las líneas cambiadas: útil para pasárselas a una IA. */
|
|
128
|
+
export function changedTokens(diff) {
|
|
129
|
+
return estimateTokens(diff
|
|
130
|
+
.filter((d) => d.type !== "equal")
|
|
131
|
+
.map((d) => d.text)
|
|
132
|
+
.join("\n"));
|
|
133
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Causa de un fallo, para poder dar un mensaje accionable al usuario. */
|
|
2
|
+
export type ErrorCode = "encrypted" | "corrupt" | "generic";
|
|
3
|
+
/**
|
|
4
|
+
* Traduce el error de pdf-lib o PDF.js a una causa. pdf-lib lanza
|
|
5
|
+
* EncryptedPDFError y PDF.js PasswordException para documentos con contraseña.
|
|
6
|
+
*/
|
|
7
|
+
export declare function classifyError(e: unknown): ErrorCode;
|
|
8
|
+
/** Error de una operación PDF, con la causa ya clasificada. */
|
|
9
|
+
export declare class PdfError extends Error {
|
|
10
|
+
code: ErrorCode;
|
|
11
|
+
constructor(message: string, code: ErrorCode);
|
|
12
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Traduce el error de pdf-lib o PDF.js a una causa. pdf-lib lanza
|
|
3
|
+
* EncryptedPDFError y PDF.js PasswordException para documentos con contraseña.
|
|
4
|
+
*/
|
|
5
|
+
export function classifyError(e) {
|
|
6
|
+
const name = e instanceof Error ? e.name : "";
|
|
7
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
8
|
+
if (name === "PasswordException" || name === "EncryptedPDFError")
|
|
9
|
+
return "encrypted";
|
|
10
|
+
if (/encrypt|password|contrase/i.test(message))
|
|
11
|
+
return "encrypted";
|
|
12
|
+
if (/invalid pdf|no pdf header|corrupt|malformed|unexpected end/i.test(message))
|
|
13
|
+
return "corrupt";
|
|
14
|
+
return "generic";
|
|
15
|
+
}
|
|
16
|
+
/** Error de una operación PDF, con la causa ya clasificada. */
|
|
17
|
+
export class PdfError extends Error {
|
|
18
|
+
code;
|
|
19
|
+
constructor(message, code) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "PdfError";
|
|
22
|
+
this.code = code;
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export * from "./anonymize.js";
|
|
2
|
+
export * from "./ranges.js";
|
|
3
|
+
export * from "./errors.js";
|
|
4
|
+
export * from "./merge.js";
|
|
5
|
+
export * from "./split.js";
|
|
6
|
+
export * from "./organize.js";
|
|
7
|
+
export * from "./lossless.js";
|
|
8
|
+
export * from "./markdown.js";
|
|
9
|
+
export * from "./ocr.js";
|
|
10
|
+
export * from "./chunk.js";
|
|
11
|
+
export * from "./tables.js";
|
|
12
|
+
export * from "./diff.js";
|
|
13
|
+
export * from "./text.js";
|
|
14
|
+
export * from "./dedupe.js";
|
|
15
|
+
export * from "./contamination.js";
|
|
16
|
+
export * from "./datasplit.js";
|
|
17
|
+
export * from "./dataset.js";
|
|
18
|
+
export * from "./verifiable.js";
|
|
19
|
+
export * from "./paragraphs.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// @sirdaspdf/core — motor PDF compartido por la web, la CLI y el servidor MCP.
|
|
2
|
+
// Solo depende de pdf-lib; la parte de PDF.js recibe el documento ya abierto.
|
|
3
|
+
export * from "./anonymize.js";
|
|
4
|
+
export * from "./ranges.js";
|
|
5
|
+
export * from "./errors.js";
|
|
6
|
+
export * from "./merge.js";
|
|
7
|
+
export * from "./split.js";
|
|
8
|
+
export * from "./organize.js";
|
|
9
|
+
export * from "./lossless.js";
|
|
10
|
+
export * from "./markdown.js";
|
|
11
|
+
export * from "./ocr.js";
|
|
12
|
+
export * from "./chunk.js";
|
|
13
|
+
export * from "./tables.js";
|
|
14
|
+
export * from "./diff.js";
|
|
15
|
+
export * from "./text.js";
|
|
16
|
+
export * from "./dedupe.js";
|
|
17
|
+
export * from "./contamination.js";
|
|
18
|
+
export * from "./datasplit.js";
|
|
19
|
+
export * from "./dataset.js";
|
|
20
|
+
export * from "./verifiable.js";
|
|
21
|
+
export * from "./paragraphs.js";
|
package/dist/lossless.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PDFDocument } from "pdf-lib";
|
|
2
|
+
/** Compresión estructural sin pérdida. Nunca devuelve algo más pesado que el original. */
|
|
3
|
+
export async function compressLossless(buf) {
|
|
4
|
+
const doc = await PDFDocument.load(buf);
|
|
5
|
+
doc.setProducer("Sirdas");
|
|
6
|
+
doc.setCreator("Sirdas");
|
|
7
|
+
const out = await doc.save({ useObjectStreams: true, addDefaultPage: false });
|
|
8
|
+
const original = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
9
|
+
return out.byteLength < original.byteLength ? out : original;
|
|
10
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PDFDocumentProxy } from "pdfjs-dist";
|
|
2
|
+
export interface Line {
|
|
3
|
+
text: string;
|
|
4
|
+
size: number;
|
|
5
|
+
y: number;
|
|
6
|
+
x: number;
|
|
7
|
+
page: number;
|
|
8
|
+
bold: boolean;
|
|
9
|
+
/** Posición vertical relativa: 0 = pie de página, 1 = parte superior. */
|
|
10
|
+
rel: number;
|
|
11
|
+
}
|
|
12
|
+
type Progress = (ratio: number) => void;
|
|
13
|
+
/** Extrae las líneas de texto con tamaño y posición de un documento abierto. */
|
|
14
|
+
export declare function extractLines(pdf: PDFDocumentProxy, onProgress?: Progress): Promise<Line[]>;
|
|
15
|
+
/** Convierte líneas ya extraídas en Markdown (encabezados, listas, párrafos, campos). */
|
|
16
|
+
export declare function linesToMarkdown(lines: Line[]): string;
|
|
17
|
+
/** Documento PDF.js abierto → Markdown. */
|
|
18
|
+
export declare function pdfToMarkdown(pdf: PDFDocumentProxy, onProgress?: Progress): Promise<string>;
|
|
19
|
+
/** Estimación rápida de tokens (~4 caracteres por token). */
|
|
20
|
+
export declare function estimateTokens(text: string): number;
|
|
21
|
+
export {};
|