update
This commit is contained in:
@@ -19,6 +19,7 @@ RUN git clone --depth 1 https://github.com/ggml-org/llama.cpp /src
|
|||||||
RUN cmake /src -B /build \
|
RUN cmake /src -B /build \
|
||||||
-DGGML_CUDA=ON \
|
-DGGML_CUDA=ON \
|
||||||
-DCMAKE_CUDA_ARCHITECTURES=120 \
|
-DCMAKE_CUDA_ARCHITECTURES=120 \
|
||||||
|
-DGGML_CUDA_FORCE_CUBLAS=OFF \
|
||||||
-DBUILD_SHARED_LIBS=OFF \
|
-DBUILD_SHARED_LIBS=OFF \
|
||||||
-DLLAMA_CURL=ON \
|
-DLLAMA_CURL=ON \
|
||||||
&& cmake --build /build --config Release -j --target llama-server
|
&& cmake --build /build --config Release -j --target llama-server
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ const execFileP = promisify(execFile);
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
interface Args {
|
interface Args {
|
||||||
input: string;
|
|
||||||
pdfRoot: string;
|
pdfRoot: string;
|
||||||
api: string;
|
api: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
@@ -60,15 +59,14 @@ function parseArgs(): Args {
|
|||||||
const i = a.indexOf(flag);
|
const i = a.indexOf(flag);
|
||||||
return i >= 0 && a[i + 1] !== undefined ? a[i + 1] : def;
|
return i >= 0 && a[i + 1] !== undefined ? a[i + 1] : def;
|
||||||
};
|
};
|
||||||
const input = get("--input", "out/buyers.json")!;
|
|
||||||
const pdfRoot = get("--pdf-root");
|
const pdfRoot = get("--pdf-root");
|
||||||
const api = (get("--api", "http://localhost:8000/v1") || "").replace(/\/+$/, "");
|
const api = (get("--api", "http://localhost:8000/v1") || "").replace(/\/+$/, "");
|
||||||
if (!pdfRoot) {
|
if (!pdfRoot) {
|
||||||
console.error("Fehler: --pdf-root <Verzeichnis> ist erforderlich.");
|
console.error("Fehler: --pdf-root <Verzeichnis> ist erforderlich.");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
throw new Error("unreachable"); // hilft dem TS-Narrowing
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
input,
|
|
||||||
pdfRoot: pdfRoot.replace(/^~(?=\/)/, os.homedir()),
|
pdfRoot: pdfRoot.replace(/^~(?=\/)/, os.homedir()),
|
||||||
api,
|
api,
|
||||||
limit: parseInt(get("--limit", "0")!, 10) || 0,
|
limit: parseInt(get("--limit", "0")!, 10) || 0,
|
||||||
@@ -77,9 +75,10 @@ function parseArgs(): Args {
|
|||||||
const v = get("--dpi", "auto")!;
|
const v = get("--dpi", "auto")!;
|
||||||
return v === "auto" ? "auto" : parseInt(v, 10) || 150;
|
return v === "auto" ? "auto" : parseInt(v, 10) || 150;
|
||||||
})(),
|
})(),
|
||||||
maxPages: parseInt(get("--max-pages", "8")!, 10) || 8,
|
// Nur PDFs mit HOECHSTENS so vielen Seiten werden per Vision gescannt.
|
||||||
|
maxPages: parseInt(get("--max-pages", "10")!, 10) || 10,
|
||||||
force: a.includes("--force"),
|
force: a.includes("--force"),
|
||||||
outDir: get("--out-dir", path.dirname(input))!,
|
outDir: get("--out-dir", "out")!,
|
||||||
timeoutMs: (parseInt(get("--timeout", "180")!, 10) || 180) * 1000,
|
timeoutMs: (parseInt(get("--timeout", "180")!, 10) || 180) * 1000,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -223,6 +222,17 @@ async function buildPdfIndex(root: string): Promise<Map<string, string>> {
|
|||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Seitenzahl via pdfinfo (poppler-utils). -1 bei Fehler/beschaedigtem PDF. */
|
||||||
|
async function pdfPageCount(pdfPath: string): Promise<number> {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execFileP("pdfinfo", [pdfPath], { timeout: 30_000 });
|
||||||
|
const m = stdout.match(/^Pages:\s+(\d+)/m);
|
||||||
|
return m ? parseInt(m[1], 10) : -1;
|
||||||
|
} catch {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function renderPdf(pdfPath: string, dpi: number, maxPages: number, tmpDir: string): Promise<string[]> {
|
async function renderPdf(pdfPath: string, dpi: number, maxPages: number, tmpDir: string): Promise<string[]> {
|
||||||
const prefix = path.join(tmpDir, "page");
|
const prefix = path.join(tmpDir, "page");
|
||||||
await execFileP("pdftoppm", ["-png", "-r", String(dpi), "-l", String(maxPages), pdfPath, prefix], {
|
await execFileP("pdftoppm", ["-png", "-r", String(dpi), "-l", String(maxPages), pdfPath, prefix], {
|
||||||
@@ -465,10 +475,10 @@ async function main(): Promise<void> {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const all: BuyerRecord[] = JSON.parse(await fsp.readFile(args.input, "utf8"));
|
|
||||||
|
|
||||||
const visionPath = path.join(args.outDir, "buyers_vision.json");
|
const visionPath = path.join(args.outDir, "buyers_vision.json");
|
||||||
const mergedPath = path.join(args.outDir, "buyers_merged.json");
|
await fsp.mkdir(args.outDir, { recursive: true });
|
||||||
|
|
||||||
|
// Resume-Stand laden (bereits verarbeitete Dokumente)
|
||||||
const done = new Map<string, BuyerRecord>();
|
const done = new Map<string, BuyerRecord>();
|
||||||
try {
|
try {
|
||||||
const prev: BuyerRecord[] = JSON.parse(await fsp.readFile(visionPath, "utf8"));
|
const prev: BuyerRecord[] = JSON.parse(await fsp.readFile(visionPath, "utf8"));
|
||||||
@@ -477,21 +487,64 @@ async function main(): Promise<void> {
|
|||||||
/* kein Resume-Stand */
|
/* kein Resume-Stand */
|
||||||
}
|
}
|
||||||
|
|
||||||
let candidates = all.filter((r) => r.is_buyer_sheet === false);
|
// ---------------------------------------------------------------------
|
||||||
|
// PHASE 1 — INDEX: alle PDFs scannen, file_name + _pages_total sofort
|
||||||
|
// ins buyers_vision.json eintragen (auch die zu grossen, dann markiert).
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
console.error(`Phase 1: Indexiere PDFs unter ${args.pdfRoot} ...`);
|
||||||
|
const pdfIndex = await buildPdfIndex(args.pdfRoot);
|
||||||
|
const names = [...pdfIndex.keys()].sort();
|
||||||
|
console.error(`${names.length} PDFs gefunden. Ermittle Seitenzahlen ...`);
|
||||||
|
|
||||||
|
let idx = 0;
|
||||||
|
for (const name of names) {
|
||||||
|
idx++;
|
||||||
|
// schon indexiert (mit gueltiger Seitenzahl)? dann nicht neu zaehlen
|
||||||
|
const existing = done.get(name);
|
||||||
|
if (existing && typeof existing["_pages_total"] === "number" && existing["_pages_total"]! >= 0 && !args.force) {
|
||||||
|
if (idx % 50 === 0) progress(`Index ${idx}/${names.length}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const pdfPath = pdfIndex.get(name)!;
|
||||||
|
const pages = await pdfPageCount(pdfPath);
|
||||||
|
const tooMany = pages < 0 ? false : pages > args.maxPages;
|
||||||
|
const prev = done.get(name) ?? { file_name: name, is_buyer_sheet: false };
|
||||||
|
done.set(name, {
|
||||||
|
...prev,
|
||||||
|
file_name: name,
|
||||||
|
_pages_total: pages,
|
||||||
|
...(pages < 0 ? { _index_error: "pdfinfo fehlgeschlagen (beschaedigt?)" } : {}),
|
||||||
|
...(tooMany ? { _skipped_too_many_pages: true } : {}),
|
||||||
|
});
|
||||||
|
if (idx % 25 === 0 || idx === names.length) progress(`Index ${idx}/${names.length}`);
|
||||||
|
}
|
||||||
|
// Index sofort persistieren, bevor die teure Phase 2 startet
|
||||||
|
await fsp.writeFile(visionPath, JSON.stringify([...done.values()], null, 2));
|
||||||
|
process.stderr.write("\n");
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// PHASE 2 — INHALT: nur PDFs <= maxPages, noch nicht (fehlerfrei) erledigt.
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
let candidates = [...done.values()].filter((r) => {
|
||||||
|
const pages = r["_pages_total"] as number | undefined;
|
||||||
|
return typeof pages === "number" && pages > 0 && pages <= args.maxPages;
|
||||||
|
});
|
||||||
if (args.only) candidates = candidates.filter((r) => r.file_name === args.only);
|
if (args.only) candidates = candidates.filter((r) => r.file_name === args.only);
|
||||||
|
|
||||||
// Resume-Skip VOR dem Limit: bereits fehlerfrei Verarbeitete zählen nicht mit
|
// Resume-Skip VOR dem Limit: fehlerfrei mit echtem Vision-Ergebnis = fertig
|
||||||
const pending = candidates.filter((r) => {
|
const pending = candidates.filter((r) => {
|
||||||
const prev = done.get(r.file_name);
|
const prev = done.get(r.file_name);
|
||||||
return !(prev && !prev["_vision_error"] && !args.force);
|
const hasResult = prev && prev["_parser"] === "vision" && !prev["_vision_error"];
|
||||||
|
return !(hasResult && !args.force);
|
||||||
});
|
});
|
||||||
const skipped = candidates.length - pending.length;
|
const skipped = candidates.length - pending.length;
|
||||||
const targets = args.limit > 0 ? pending.slice(0, args.limit) : pending;
|
const targets = args.limit > 0 ? pending.slice(0, args.limit) : pending;
|
||||||
|
|
||||||
console.error(`Indexiere PDFs unter ${args.pdfRoot} ...`);
|
const tooManyCount = [...done.values()].filter((r) => r["_skipped_too_many_pages"]).length;
|
||||||
const pdfIndex = await buildPdfIndex(args.pdfRoot);
|
|
||||||
console.error(
|
console.error(
|
||||||
`${pdfIndex.size} PDFs gefunden. ${candidates.length} Kandidaten, ${skipped} bereits verarbeitet, ${targets.length} in diesem Lauf.`
|
`Phase 2: ${candidates.length} Kandidaten (<=${args.maxPages} Seiten), ` +
|
||||||
|
`${tooManyCount} zu gross (uebersprungen), ${skipped} bereits verarbeitet, ` +
|
||||||
|
`${targets.length} in diesem Lauf.`
|
||||||
);
|
);
|
||||||
|
|
||||||
const model = await fetchModelId(args.api);
|
const model = await fetchModelId(args.api);
|
||||||
@@ -560,21 +613,17 @@ async function main(): Promise<void> {
|
|||||||
await fsp.rm(tmpDir, { recursive: true, force: true });
|
await fsp.rm(tmpDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inkrementell sichern (Resume-fähig) — merged gleich mit
|
// Inkrementell sichern (Resume-fähig)
|
||||||
await fsp.mkdir(args.outDir, { recursive: true });
|
|
||||||
await fsp.writeFile(visionPath, JSON.stringify([...done.values()], null, 2));
|
await fsp.writeFile(visionPath, JSON.stringify([...done.values()], null, 2));
|
||||||
await fsp.writeFile(mergedPath, JSON.stringify(all.map((r) => done.get(r.file_name) ?? r), null, 2));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge: Gleis A + Gleis B
|
await fsp.writeFile(visionPath, JSON.stringify([...done.values()], null, 2));
|
||||||
const merged = all.map((r) => done.get(r.file_name) ?? r);
|
|
||||||
await fsp.writeFile(mergedPath, JSON.stringify(merged, null, 2));
|
|
||||||
|
|
||||||
process.stderr.write("\n");
|
process.stderr.write("\n");
|
||||||
console.error(
|
console.error(
|
||||||
`Fertig. OK: ${ok}, CA only: ${caOnly}, kein Buyer Sheet: ${notSheet}, Fehler: ${errors}, übersprungen: ${skipped}`
|
`Fertig. OK: ${ok}, CA only: ${caOnly}, kein Buyer Sheet: ${notSheet}, Fehler: ${errors}, übersprungen: ${skipped}`
|
||||||
);
|
);
|
||||||
console.error(`→ ${visionPath}\n→ ${mergedPath}`);
|
console.error(`→ ${visionPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((e) => {
|
main().catch((e) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user