@clazic/kordoc 2.5.0 → 2.5.2
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/dist/batch-provider-XRF6F26E.js +234 -0
- package/dist/batch-provider-XRF6F26E.js.map +1 -0
- package/dist/{chunk-NKUNXGWI.js → chunk-25ZYYLVP.js} +8 -6
- package/dist/chunk-25ZYYLVP.js.map +1 -0
- package/dist/{chunk-IJGNPAK2.js → chunk-5CILZHRW.js} +2 -2
- package/dist/chunk-S7BHLD2V.js +200 -0
- package/dist/{chunk-Y4WFKJ5P.js.map → chunk-S7BHLD2V.js.map} +1 -1
- package/dist/cli.js +6 -38
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +473 -2816
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +482 -2826
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +3 -6
- package/dist/mcp.js.map +1 -1
- package/dist/{resolve-XWYJYKKH.js → resolve-ZSUEJK3E.js} +4 -4
- package/dist/{utils-RBXHHCLI.js → utils-H2BL5GNR.js} +2 -2
- package/dist/{watch-FRLS6FKE.js → watch-D6ODQLPJ.js} +4 -7
- package/dist/{watch-FRLS6FKE.js.map → watch-D6ODQLPJ.js.map} +1 -1
- package/package.json +3 -7
- package/dist/batch-provider-5BFJRKAZ.js +0 -190
- package/dist/batch-provider-5BFJRKAZ.js.map +0 -1
- package/dist/chunk-AEWWERJ5.js +0 -35
- package/dist/chunk-AEWWERJ5.js.map +0 -1
- package/dist/chunk-CPTOBJJD.js +0 -125
- package/dist/chunk-CPTOBJJD.js.map +0 -1
- package/dist/chunk-NKUNXGWI.js.map +0 -1
- package/dist/chunk-THBLCND6.js +0 -33
- package/dist/chunk-THBLCND6.js.map +0 -1
- package/dist/chunk-Y4WFKJ5P.js +0 -167
- package/dist/doctor-SJ7NYSXC.js +0 -126
- package/dist/doctor-SJ7NYSXC.js.map +0 -1
- package/dist/install-commands-P2KTEXQ4.js +0 -11
- package/dist/pm-7KGLH6MX.js +0 -9
- package/dist/pm-7KGLH6MX.js.map +0 -1
- package/dist/setup/doctor.cjs +0 -308
- package/dist/setup/doctor.js +0 -288
- package/dist/utils-RBXHHCLI.js.map +0 -1
- package/scripts/postinstall.cjs +0 -27
- /package/dist/{chunk-IJGNPAK2.js.map → chunk-5CILZHRW.js.map} +0 -0
- /package/dist/{resolve-XWYJYKKH.js.map → resolve-ZSUEJK3E.js.map} +0 -0
- /package/dist/{install-commands-P2KTEXQ4.js.map → utils-H2BL5GNR.js.map} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/utils.ts
|
|
4
|
-
var VERSION = true ? "2.5.
|
|
4
|
+
var VERSION = true ? "2.5.1" : "0.0.0-dev";
|
|
5
5
|
function toArrayBuffer(buf) {
|
|
6
6
|
if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) {
|
|
7
7
|
return buf.buffer;
|
|
@@ -105,4 +105,4 @@ export {
|
|
|
105
105
|
classifyError,
|
|
106
106
|
normalizeKordocError
|
|
107
107
|
};
|
|
108
|
-
//# sourceMappingURL=chunk-
|
|
108
|
+
//# sourceMappingURL=chunk-5CILZHRW.js.map
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/ocr/cli-provider.ts
|
|
4
|
+
import { spawnSync } from "child_process";
|
|
5
|
+
import { writeFileSync, readFileSync, unlinkSync, mkdirSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { tmpdir } from "os";
|
|
8
|
+
var OCR_PROMPT = `\uC774 PDF \uD398\uC774\uC9C0 \uC774\uBBF8\uC9C0\uC5D0\uC11C \uD14D\uC2A4\uD2B8\uC640 \uD45C\uB97C \uCD94\uCD9C\uD558\uC5EC \uC21C\uC218 Markdown\uC73C\uB85C \uBCC0\uD658\uD558\uACE0, \uBA85\uBC31\uD55C OCR \uC624\uC778\uC2DD\uACFC \uD45C \uAD6C\uC870 \uAE68\uC9D0\uB9CC \uAD50\uC815\uD558\uC5EC \uCD5C\uC885 \uACB0\uACFC\uB97C \uCD9C\uB825\uD558\uC138\uC694.
|
|
9
|
+
|
|
10
|
+
[\uAE30\uBCF8 \uCD94\uCD9C \uADDC\uCE59]
|
|
11
|
+
- \uBCF8\uBB38, \uD45C, \uC81C\uBAA9, \uB9AC\uC2A4\uD2B8, \uCEA1\uC158\uC744 \uC6D0\uBB38 \uAD6C\uC870 \uADF8\uB300\uB85C Markdown\uC73C\uB85C \uBCC0\uD658
|
|
12
|
+
- \uD5E4\uB529\uC740 \uC2DC\uAC01\uC801 \uD06C\uAE30\xB7\uAD75\uAE30\uC5D0 \uB530\uB77C # ~ ###### \uC0AC\uC6A9
|
|
13
|
+
- \uB9AC\uC2A4\uD2B8\uB294 -, 1. \uC0AC\uC6A9 (\uC6D0\uBB38 \uBC88\uD638 \uCCB4\uACC4\uAC00 \u2460, \uAC00., 1) \uB4F1\uC774\uBA74 \uADF8 \uD45C\uAE30 \uC720\uC9C0)
|
|
14
|
+
- \uC774\uBBF8\uC9C0\xB7\uB3C4\uD615\xB7\uB85C\uACE0\xB7\uD398\uC774\uC9C0\uBC88\uD638\xB7\uBA38\uB9AC\uAE00/\uBC14\uB2E5\uAE00\uC740 \uBB34\uC2DC
|
|
15
|
+
- \uC6D0\uBB38\uC758 \uC77D\uAE30 \uC21C\uC11C(\uC88C\u2192\uC6B0, \uC704\u2192\uC544\uB798, \uB2E4\uB2E8\uC774\uBA74 \uB2E8\uBCC4\uB85C)\uB97C \uC720\uC9C0
|
|
16
|
+
|
|
17
|
+
[\uD45C \uCD94\uCD9C \uADDC\uCE59 \u2014 \uAC00\uC7A5 \uC911\uC694]
|
|
18
|
+
- \uD45C\uB294 \uBC18\uB4DC\uC2DC Markdown \uD30C\uC774\uD504 \uD14C\uC774\uBE14 \uBB38\uBC95 \uC0AC\uC6A9: \uD5E4\uB354 \uD589 + |---| \uAD6C\uBD84\uC120 + \uB370\uC774\uD130 \uD589
|
|
19
|
+
- \uC2DC\uAC01\uC801\uC73C\uB85C \uAD75\uAC70\uB098 \uC74C\uC601(\uD68C\uC0C9/\uC0C9\uC0C1) \uBC30\uACBD\uC774 \uC788\uB294 \uD589\uC744 \uD5E4\uB354\uB85C \uC2DD\uBCC4. \uD5E4\uB354\uAC00 \uC5C6\uB294 \uD45C\uB77C\uB3C4 \uCCAB \uD589\uC744 \uD5E4\uB354\uB85C \uB450\uACE0 |---| \uAD6C\uBD84\uC120 \uCD94\uAC00
|
|
20
|
+
- \uBAA8\uB4E0 \uD589\uC758 \uD30C\uC774\uD504(|) \uAC1C\uC218\uB97C \uD5E4\uB354\uC640 \uB3D9\uC77C\uD558\uAC8C \uB9DE\uCD9C \uAC83 \u2014 \uBE48 \uC140\uC740 \uACF5\uBC31\uC73C\uB85C \uCC44\uC6B0\uACE0 \uC808\uB300 \uC140\uC744 \uC0DD\uB7B5\uD558\uC9C0 \uB9D0 \uAC83
|
|
21
|
+
- \uC140 \uC548\uC758 \uC904\uBC14\uAFC8\uC740 <br>\uB85C \uD45C\uAE30 (\uC2E4\uC81C \uAC1C\uD589 \uBB38\uC790 \uC0AC\uC6A9 \uAE08\uC9C0 \u2014 \uD45C\uAC00 \uAE68\uC9D0)
|
|
22
|
+
- \uBCD1\uD569 \uC140 \uCC98\uB9AC:
|
|
23
|
+
- \uAC00\uB85C \uBCD1\uD569(colspan): \uBCD1\uD569\uB41C \uC140 \uB0B4\uC6A9\uC744 \uCCAB \uCE78\uC5D0 \uC4F0\uACE0 \uB098\uBA38\uC9C0 \uCE78\uC740 \uBE48 \uCE78\uC73C\uB85C \uB458 \uAC83
|
|
24
|
+
- \uC138\uB85C \uBCD1\uD569(rowspan): \uBCD1\uD569\uB41C \uBAA8\uB4E0 \uD589\uC758 \uD574\uB2F9 \uCE78\uC5D0 \uB3D9\uC77C\uD55C \uB0B4\uC6A9\uC744 \uBC18\uBCF5 \uAE30\uC7AC\uD560 \uAC83
|
|
25
|
+
- 2\uB2E8 \uD5E4\uB354(\uD5E4\uB354\uAC00 \uB450 \uC904\uC778 \uACBD\uC6B0): \uC0C1\uC704/\uD558\uC704 \uD5E4\uB354\uB97C " / "\uB85C \uD569\uCCD0 \uB2E8\uC77C \uD5E4\uB354 \uD589\uC73C\uB85C \uB9CC\uB4E4 \uAC83 (\uC608: "2024 / 1\uBD84\uAE30")
|
|
26
|
+
- \uD45C \uC704\xB7\uC544\uB798\uC758 \uCEA1\uC158(\uC608: "<\uD45C 1> ...", "[\uD45C 2-1]")\uC740 \uD45C \uBC14\uB85C \uC704\uC5D0 \uBCC4\uB3C4 \uC904\uB85C \uBCF4\uC874
|
|
27
|
+
- \uD55C \uD398\uC774\uC9C0\uC5D0\uC11C \uD45C\uAC00 \uC911\uAC04\uC5D0 \uB04A\uC5B4\uC838 \uBCF4\uC774\uB354\uB77C\uB3C4, \uD5E4\uB354\uAC00 \uB3D9\uC77C\uD558\uBA74 \uD558\uB098\uC758 \uD45C\uB85C \uC774\uC5B4\uBD99\uC77C \uAC83
|
|
28
|
+
- \uC88C\uCE21 \uCCAB \uCEEC\uB7FC\uC774 \uC138\uB85C\uC4F0\uAE30 \uB77C\uBCA8(\uAD6C\uBD84/\uBD84\uB958/\uC5F0\uB3C4 \uB4F1)\uC774\uB77C\uB3C4 \uC77C\uBC18 \uC140\uB85C \uBCC0\uD658\uD558\uC5EC \uB204\uB77D \uAE08\uC9C0
|
|
29
|
+
|
|
30
|
+
[OCR \uC624\uC778\uC2DD\xB7\uAE68\uC9D0 \uAD50\uC815 \u2014 \uD5C8\uC6A9 \uBC94\uC704\uB9CC]
|
|
31
|
+
- \uC22B\uC790 \uCE78\uC5D0\uC11C 'O'\u2192'0', 'l/I'\u2192'1' \uB4F1 \uB9E5\uB77D\uC0C1 \uBA85\uBC31\uD55C \uC624\uC778\uC2DD\uB9CC \uAD50\uC815
|
|
32
|
+
- \uB2E8\uC5B4 \uC911\uAC04\uC5D0 \uC798\uBABB \uC0BD\uC785\uB41C \uACF5\uBC31 \uC81C\uAC70 (\uC608: "\uC8FC\uAC70 \uC885 \uD569 \uACC4\uD68D" \u2192 "\uC8FC\uAC70\uC885\uD569\uACC4\uD68D")
|
|
33
|
+
- \uC904\uBC14\uAFC8 \uC624\uB958\uB85C \uB04A\uAE34 \uBB38\uC7A5\uC740 \uC758\uBBF8 \uB2E8\uC704\uB85C \uBCD1\uD569 (\uB2E8, \uD45C \uC140 \uB0B4\uBD80\uB294 <br> \uC720\uC9C0)
|
|
34
|
+
- \uD45C\uC5D0\uC11C \uD589 \uC5B4\uAE0B\uB0A8\uC774 \uC758\uC2EC\uB418\uBA74(\uCEEC\uB7FC \uC218 \uBD88\uC77C\uCE58) \uBE48 \uC140\uB85C \uD328\uB529\uD558\uC5EC \uCEEC\uB7FC \uC815\uD569\uC131\uC744 \uC6B0\uC120 \uD655\uBCF4
|
|
35
|
+
|
|
36
|
+
[\uC808\uB300 \uAE08\uC9C0 \uC0AC\uD56D]
|
|
37
|
+
- \uBB38\uC7A5\xB7\uB2E8\uB77D\xB7\uD56D\uBAA9\xB7\uD45C \uD589/\uC5F4\uC744 \uCD94\uAC00\uD558\uAC70\uB098 \uC0AD\uC81C\uD558\uC9C0 \uB9D0 \uAC83
|
|
38
|
+
- \uC22B\uC790, \uD37C\uC13C\uD2B8, \uB0A0\uC9DC, \uB2E8\uC704, \uAE08\uC561\uC744 \uC808\uB300 \uBCC0\uACBD\uD558\uC9C0 \uB9D0 \uAC83
|
|
39
|
+
- \uACE0\uC720\uBA85\uC0AC, \uAE30\uAD00\uBA85, \uBC95\uB839\uBA85, \uC9C0\uBA85, \uC778\uBA85\uC744 \uBCC0\uACBD\uD558\uC9C0 \uB9D0 \uAC83
|
|
40
|
+
- \uD45C\uC758 \uD5E4\uB354 \uD14D\uC2A4\uD2B8, \uCEA1\uC158, \uD589/\uC5F4 \uAC1C\uC218\uB97C \uC784\uC758\uB85C \uBC14\uAFB8\uC9C0 \uB9D0 \uAC83
|
|
41
|
+
- \uCCB4\uD06C\uBC15\uC2A4(\u2611/\u2610/\u25A0/\u25A1), \uD2B9\uC218\uBB38\uC790(\u203B, \u2460\u2461\u2462, \u3260, \uAC00., \uB098.)\uB97C \uC784\uC758\uB85C \uBC14\uAFB8\uC9C0 \uB9D0 \uAC83
|
|
42
|
+
- \uC6D0\uBB38\uC5D0 \uC5C6\uB294 \uB0B4\uC6A9\uC744 \uC694\uC57D\xB7\uBCF4\uC644\xB7\uCD94\uB860\uD558\uC9C0 \uB9D0 \uAC83
|
|
43
|
+
- \`\`\`, \`\`\`markdown \uAC19\uC740 \uCF54\uB4DC\uD39C\uC2A4\uB85C \uAC10\uC2F8\uC9C0 \uB9D0 \uAC83
|
|
44
|
+
- \uC124\uBA85\xB7\uC8FC\uC11D\xB7\uBA54\uD0C0 \uBB38\uC7A5 \uCD94\uAC00 \uAE08\uC9C0
|
|
45
|
+
|
|
46
|
+
[\uBD88\uD655\uC2E4\uD560 \uB54C]
|
|
47
|
+
- \uAE00\uC790\uAC00 \uD750\uB9BF\uD558\uAC70\uB098 \uD310\uB3C5 \uBD88\uAC00\uD558\uBA74 \uBCF4\uC774\uB294 \uADF8\uB300\uB85C \uB450\uAC70\uB098, \uBD88\uAC00\uD53C\uD558\uBA74 \uD55C \uAE00\uC790\uB9CC ?\uB85C \uD45C\uC2DC
|
|
48
|
+
- \uD45C \uAD6C\uC870\uAC00 \uBAA8\uD638\uD558\uBA74 \uCEEC\uB7FC \uC218 \uC77C\uCE58 + \uBE48 \uC140 \uD328\uB529 \uD615\uD0DC\uB85C \uCD9C\uB825
|
|
49
|
+
- \uCD94\uCE21\uBCF4\uB2E4 \uC6D0\uBB38 \uBCF4\uC874\uC744 \uD56D\uC0C1 \uC6B0\uC120`;
|
|
50
|
+
var _tempDir = null;
|
|
51
|
+
function getTempDir() {
|
|
52
|
+
if (!_tempDir) {
|
|
53
|
+
_tempDir = join(process.cwd(), ".kordoc_ocr_tmp");
|
|
54
|
+
mkdirSync(_tempDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
return _tempDir;
|
|
57
|
+
}
|
|
58
|
+
function createCliOcrProvider(mode) {
|
|
59
|
+
return async (pageImage, pageNumber) => {
|
|
60
|
+
const tempPath = join(getTempDir(), `page-${pageNumber}.png`);
|
|
61
|
+
try {
|
|
62
|
+
writeFileSync(tempPath, pageImage);
|
|
63
|
+
let output;
|
|
64
|
+
if (mode === "ollama") {
|
|
65
|
+
output = await callOllamaApi(tempPath);
|
|
66
|
+
} else {
|
|
67
|
+
output = callCli(mode, tempPath);
|
|
68
|
+
}
|
|
69
|
+
return { markdown: stripCodeFence(output.trim()) };
|
|
70
|
+
} finally {
|
|
71
|
+
try {
|
|
72
|
+
unlinkSync(tempPath);
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function checkForLimitError(output, mode) {
|
|
79
|
+
const lower = output.toLowerCase();
|
|
80
|
+
if (lower.includes("usage limit") || lower.includes("rate limit")) {
|
|
81
|
+
throw new Error(`${mode} \uC0AC\uC6A9\uB7C9/\uC18D\uB3C4 \uC81C\uD55C: ${output.trim().slice(0, 200)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function callCli(mode, imagePath) {
|
|
85
|
+
if (mode === "codex") {
|
|
86
|
+
return callCodexCli(imagePath);
|
|
87
|
+
}
|
|
88
|
+
const args = buildCliArgs(mode, imagePath);
|
|
89
|
+
const result = spawnSync(mode, args, {
|
|
90
|
+
encoding: "utf-8",
|
|
91
|
+
timeout: 6e5,
|
|
92
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
93
|
+
shell: process.platform === "win32",
|
|
94
|
+
// claude: /tmp에서 실행하여 프로젝트 CLAUDE.md의 규칙 간섭 방지
|
|
95
|
+
...mode === "claude" ? { cwd: tmpdir() } : {}
|
|
96
|
+
});
|
|
97
|
+
if (result.error) {
|
|
98
|
+
throw new Error(`${mode} CLI \uC2E4\uD589 \uC2E4\uD328: ${result.error.message}`);
|
|
99
|
+
}
|
|
100
|
+
if (result.status !== 0) {
|
|
101
|
+
const errMsg = result.stderr?.trim() || `exit code ${result.status}`;
|
|
102
|
+
throw new Error(`${mode} OCR \uC2E4\uD328: ${errMsg}`);
|
|
103
|
+
}
|
|
104
|
+
const output = result.stdout || "";
|
|
105
|
+
checkForLimitError(output, mode);
|
|
106
|
+
return output;
|
|
107
|
+
}
|
|
108
|
+
function callCodexCli(imagePath) {
|
|
109
|
+
const outPath = join(tmpdir(), `kordoc-codex-out-${Date.now()}.txt`);
|
|
110
|
+
try {
|
|
111
|
+
const args = ["exec", OCR_PROMPT, "--image", imagePath, "--output-last-message", outPath];
|
|
112
|
+
const model = process.env.KORDOC_CODEX_MODEL;
|
|
113
|
+
if (model) args.push("--model", model);
|
|
114
|
+
const result = spawnSync("codex", args, {
|
|
115
|
+
encoding: "utf-8",
|
|
116
|
+
timeout: 18e4,
|
|
117
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
118
|
+
input: "",
|
|
119
|
+
// stdin EOF 즉시 전달 (대화형 입력 차단)
|
|
120
|
+
shell: process.platform === "win32"
|
|
121
|
+
});
|
|
122
|
+
if (result.error) {
|
|
123
|
+
throw new Error(`codex CLI \uC2E4\uD589 \uC2E4\uD328: ${result.error.message}`);
|
|
124
|
+
}
|
|
125
|
+
if (result.status !== 0) {
|
|
126
|
+
const errMsg = result.stderr?.trim() || `exit code ${result.status}`;
|
|
127
|
+
throw new Error(`codex OCR \uC2E4\uD328: ${errMsg}`);
|
|
128
|
+
}
|
|
129
|
+
let text;
|
|
130
|
+
try {
|
|
131
|
+
text = readFileSync(outPath, "utf-8");
|
|
132
|
+
} catch {
|
|
133
|
+
text = result.stdout || "";
|
|
134
|
+
}
|
|
135
|
+
checkForLimitError(text, "codex");
|
|
136
|
+
return text;
|
|
137
|
+
} finally {
|
|
138
|
+
try {
|
|
139
|
+
unlinkSync(outPath);
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function buildCliArgs(mode, imagePath) {
|
|
145
|
+
const normalizedPath = imagePath.replace(/\\/g, "/");
|
|
146
|
+
const promptWithImage = `${OCR_PROMPT}
|
|
147
|
+
|
|
148
|
+
\uC774\uBBF8\uC9C0: @${normalizedPath}`;
|
|
149
|
+
switch (mode) {
|
|
150
|
+
case "gemini": {
|
|
151
|
+
const args = ["--prompt", promptWithImage, "--yolo"];
|
|
152
|
+
const model = process.env.KORDOC_GEMINI_MODEL;
|
|
153
|
+
if (model) args.push("--model", model);
|
|
154
|
+
return args;
|
|
155
|
+
}
|
|
156
|
+
case "claude": {
|
|
157
|
+
const args = ["--print", promptWithImage];
|
|
158
|
+
const model = process.env.KORDOC_CLAUDE_MODEL;
|
|
159
|
+
if (model) args.push("--model", model);
|
|
160
|
+
return args;
|
|
161
|
+
}
|
|
162
|
+
default:
|
|
163
|
+
throw new Error(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 CLI: ${mode}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function callOllamaApi(imagePath) {
|
|
167
|
+
const { readFileSync: readFileSync2 } = await import("fs");
|
|
168
|
+
const imageBase64 = readFileSync2(imagePath).toString("base64");
|
|
169
|
+
const model = process.env.KORDOC_OLLAMA_MODEL || "qwen3-vl:8b";
|
|
170
|
+
const host = process.env.KORDOC_OLLAMA_HOST || "http://localhost:11434";
|
|
171
|
+
const timeoutMs = Number(process.env.KORDOC_OLLAMA_TIMEOUT) || 12e4;
|
|
172
|
+
const response = await fetch(`${host}/api/chat`, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: { "Content-Type": "application/json" },
|
|
175
|
+
body: JSON.stringify({
|
|
176
|
+
model,
|
|
177
|
+
messages: [{
|
|
178
|
+
role: "user",
|
|
179
|
+
content: OCR_PROMPT,
|
|
180
|
+
images: [imageBase64]
|
|
181
|
+
}],
|
|
182
|
+
stream: false
|
|
183
|
+
}),
|
|
184
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
185
|
+
});
|
|
186
|
+
if (!response.ok) {
|
|
187
|
+
throw new Error(`Ollama API \uC624\uB958: ${response.status} ${response.statusText}`);
|
|
188
|
+
}
|
|
189
|
+
const data = await response.json();
|
|
190
|
+
return data.message?.content || "";
|
|
191
|
+
}
|
|
192
|
+
function stripCodeFence(text) {
|
|
193
|
+
const match = text.match(/^```(?:markdown|md)?\s*([\s\S]*?)```\s*$/m);
|
|
194
|
+
return match ? match[1].trim() : text;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export {
|
|
198
|
+
createCliOcrProvider
|
|
199
|
+
};
|
|
200
|
+
//# sourceMappingURL=chunk-S7BHLD2V.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ocr/cli-provider.ts"],"sourcesContent":["/**\n * CLI 기반 OCR 프로바이더\n *\n * gemini / claude / codex / ollama CLI를 subprocess로 호출하여\n * PDF 페이지 이미지를 Markdown으로 변환.\n *\n * 이미지 전달 방식:\n * - gemini: -p \"프롬프트 @이미지경로\" (@ 파일 참조)\n * - claude: -p \"프롬프트 @이미지경로\" (@ 파일 참조, --print 모드)\n * - codex: exec -i 이미지경로 \"프롬프트\" (-i/--image 플래그)\n * - ollama: REST API (localhost:11434) — CLI는 이미지 입력 미지원\n */\n\nimport { spawnSync } from \"child_process\"\nimport { writeFileSync, readFileSync, unlinkSync, mkdirSync } from \"fs\"\nimport { join } from \"path\"\nimport { tmpdir } from \"os\"\nimport type { OcrMode, StructuredOcrResult } from \"../types.js\"\n\n/** OCR 프롬프트 — 모든 CLI 공통 */\nconst OCR_PROMPT = `이 PDF 페이지 이미지에서 텍스트와 테이블을 추출하여 순수 Markdown으로 변환하세요.\n규칙:\n- 테이블은 Markdown 테이블 문법 사용 (| 구분, |---|---| 헤더 구분선 포함)\n- 병합된 셀은 해당 위치에 내용 기재\n- 헤딩은 글자 크기에 따라 ## ~ ###### 사용\n- 리스트는 - 또는 1. 사용\n- 이미지, 도형 등 비텍스트 요소는 무시\n- 원문의 읽기 순서와 구조를 유지\n- \\`\\`\\`로 감싸지 말고 순수 Markdown만 출력`\n\n/** 임시 디렉토리 (프로세스당 1회 생성)\n *\n * gemini CLI는 /tmp/ 등 시스템 임시 디렉토리를 워크스페이스 외부로 간주하여\n * @파일참조 시 접근을 거부할 수 있음. cwd 하위 폴더를 사용하면 모든 CLI에서 접근 가능.\n *\n * ⚠️ .gitignore에 포함된 경로(예: .kordoc-tmp/)는 gemini CLI가 무시하므로\n * 반드시 gitignore되지 않는 이름 사용. 파일은 try/finally로 즉시 정리.\n */\nlet _tempDir: string | null = null\nfunction getTempDir(): string {\n if (!_tempDir) {\n _tempDir = join(process.cwd(), \".kordoc_ocr_tmp\")\n mkdirSync(_tempDir, { recursive: true })\n }\n return _tempDir\n}\n\n/**\n * CLI OcrProvider 생성.\n *\n * @param mode - 사용할 CLI (gemini, claude, codex, ollama)\n * @returns OcrProvider 함수 (StructuredOcrResult 반환)\n */\nexport function createCliOcrProvider(\n mode: Exclude<OcrMode, \"auto\" | \"off\">\n): (pageImage: Uint8Array, pageNumber: number, mimeType: \"image/png\") => Promise<StructuredOcrResult> {\n return async (pageImage: Uint8Array, pageNumber: number): Promise<StructuredOcrResult> => {\n const tempPath = join(getTempDir(), `page-${pageNumber}.png`)\n\n try {\n writeFileSync(tempPath, pageImage)\n\n let output: string\n if (mode === \"ollama\") {\n output = await callOllamaApi(tempPath)\n } else {\n output = callCli(mode, tempPath)\n }\n\n return { markdown: stripCodeFence(output.trim()) }\n } finally {\n try { unlinkSync(tempPath) } catch { /* 임시 파일 정리 실패 무시 */ }\n }\n }\n}\n\n/**\n * 출력 텍스트에서 사용량·속도 제한 에러 감지.\n * 해당 메시지가 포함된 경우 throw하여 다음 엔진으로 fallback 트리거.\n */\nfunction checkForLimitError(output: string, mode: string): void {\n const lower = output.toLowerCase()\n if (lower.includes(\"usage limit\") || lower.includes(\"rate limit\")) {\n throw new Error(`${mode} 사용량/속도 제한: ${output.trim().slice(0, 200)}`)\n }\n}\n\n/**\n * CLI 실행 — gemini / claude / codex\n *\n * @throws CLI 실행 실패 또는 타임아웃(180초) 시 Error\n */\nfunction callCli(mode: string, imagePath: string): string {\n // codex는 --output-last-message로 대화 헤더 없는 깔끔한 출력 사용\n if (mode === \"codex\") {\n return callCodexCli(imagePath)\n }\n\n const args = buildCliArgs(mode, imagePath)\n\n const result = spawnSync(mode, args, {\n encoding: \"utf-8\",\n timeout: 600_000,\n maxBuffer: 10 * 1024 * 1024,\n shell: process.platform === \"win32\",\n // claude: /tmp에서 실행하여 프로젝트 CLAUDE.md의 규칙 간섭 방지\n ...(mode === \"claude\" ? { cwd: tmpdir() } : {}),\n })\n\n if (result.error) {\n throw new Error(`${mode} CLI 실행 실패: ${result.error.message}`)\n }\n if (result.status !== 0) {\n const errMsg = result.stderr?.trim() || `exit code ${result.status}`\n throw new Error(`${mode} OCR 실패: ${errMsg}`)\n }\n\n const output = result.stdout || \"\"\n checkForLimitError(output, mode)\n return output\n}\n\n/**\n * codex exec 실행 — --output-last-message로 대화 헤더 없는 깔끔한 출력.\n * 인자 순서: `codex exec <prompt> --image <file> --output-last-message <outfile>`\n */\nfunction callCodexCli(imagePath: string): string {\n // 출력 파일은 /tmp/ 사용 — codex sandbox는 cwd 내 쓰기를 막을 수 있음\n const outPath = join(tmpdir(), `kordoc-codex-out-${Date.now()}.txt`)\n try {\n const args = [\"exec\", OCR_PROMPT, \"--image\", imagePath, \"--output-last-message\", outPath]\n const model = process.env.KORDOC_CODEX_MODEL\n if (model) args.push(\"--model\", model)\n\n const result = spawnSync(\"codex\", args, {\n encoding: \"utf-8\",\n timeout: 180_000,\n maxBuffer: 10 * 1024 * 1024,\n input: \"\", // stdin EOF 즉시 전달 (대화형 입력 차단)\n shell: process.platform === \"win32\",\n })\n\n if (result.error) {\n throw new Error(`codex CLI 실행 실패: ${result.error.message}`)\n }\n if (result.status !== 0) {\n const errMsg = result.stderr?.trim() || `exit code ${result.status}`\n throw new Error(`codex OCR 실패: ${errMsg}`)\n }\n\n // --output-last-message 파일에서 읽기 (없으면 stdout 폴백)\n let text: string\n try {\n text = readFileSync(outPath, \"utf-8\")\n } catch {\n text = result.stdout || \"\"\n }\n checkForLimitError(text, \"codex\")\n return text\n } finally {\n try { unlinkSync(outPath) } catch { /* 무시 */ }\n }\n}\n\n/**\n * CLI별 인자 배열 생성.\n *\n * gemini: [\"--prompt\", \"프롬프트 @이미지경로\", \"--yolo\"]\n * - -y/--yolo: 자동 승인 (OCR은 도구 사용 없으므로 실질적 영향 없음)\n * - @ 파일 참조로 이미지를 컨텍스트에 포함\n *\n * claude: [\"--print\", \"프롬프트 @이미지경로\"]\n * - --print(-p): 비대화형 출력 모드\n * - @ 파일 참조로 이미지를 컨텍스트에 포함\n *\n * codex: callCodexCli()에서 별도 처리\n * - `codex exec <prompt> --image <file> --output-last-message <outfile>`\n * - 프롬프트가 --image보다 앞에 위치해야 함 (인자 순서 중요)\n *\n * ⚠️ CLI 버전에 따라 문법이 다를 수 있음. 업데이트 시 --help 재확인 필요.\n */\nfunction buildCliArgs(mode: string, imagePath: string): string[] {\n const normalizedPath = imagePath.replace(/\\\\/g, \"/\")\n const promptWithImage = `${OCR_PROMPT}\n\n이미지: @${normalizedPath}`\n\n switch (mode) {\n case \"gemini\": {\n const args = [\"--prompt\", promptWithImage, \"--yolo\"]\n const model = process.env.KORDOC_GEMINI_MODEL\n if (model) args.push(\"--model\", model)\n return args\n }\n\n case \"claude\": {\n const args = [\"--print\", promptWithImage]\n const model = process.env.KORDOC_CLAUDE_MODEL\n if (model) args.push(\"--model\", model)\n return args\n }\n\n default:\n throw new Error(`지원하지 않는 CLI: ${mode}`)\n }\n}\n\n/**\n * Ollama REST API 호출 — CLI는 이미지 입력을 지원하지 않으므로 API 직접 사용.\n *\n * 기본 모델: KORDOC_OLLAMA_MODEL 환경변수 또는 \"gemma4:27b\"\n * 기본 호스트: KORDOC_OLLAMA_HOST 환경변수 또는 \"http://localhost:11434\"\n *\n * @throws Ollama 서버 미실행 또는 응답 오류 시 Error\n */\nasync function callOllamaApi(imagePath: string): Promise<string> {\n const { readFileSync } = await import(\"fs\")\n const imageBase64 = readFileSync(imagePath).toString(\"base64\")\n\n const model = process.env.KORDOC_OLLAMA_MODEL || \"qwen3-vl:8b\"\n const host = process.env.KORDOC_OLLAMA_HOST || \"http://localhost:11434\"\n const timeoutMs = Number(process.env.KORDOC_OLLAMA_TIMEOUT) || 120_000\n\n const response = await fetch(`${host}/api/chat`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model,\n messages: [{\n role: \"user\",\n content: OCR_PROMPT,\n images: [imageBase64],\n }],\n stream: false,\n }),\n signal: AbortSignal.timeout(timeoutMs),\n })\n\n if (!response.ok) {\n throw new Error(`Ollama API 오류: ${response.status} ${response.statusText}`)\n }\n\n const data = await response.json() as { message?: { content?: string } }\n return data.message?.content || \"\"\n}\n\n/**\n * LLM 출력에서 코드 펜스 제거.\n * LLM이 가끔 결과를 ```markdown ... ``` 으로 감싸는 경우 처리.\n */\nfunction stripCodeFence(text: string): string {\n const match = text.match(/^```(?:markdown|md)?\\s*([\\s\\S]*?)```\\s*$/m)\n return match ? match[1].trim() : text\n}\n"],"mappings":"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","names":["readFileSync"]}
|
|
1
|
+
{"version":3,"sources":["../src/ocr/cli-provider.ts"],"sourcesContent":["/**\n * CLI 기반 OCR 프로바이더\n *\n * gemini / claude / codex / ollama CLI를 subprocess로 호출하여\n * PDF 페이지 이미지를 Markdown으로 변환.\n *\n * 이미지 전달 방식:\n * - gemini: -p \"프롬프트 @이미지경로\" (@ 파일 참조)\n * - claude: -p \"프롬프트 @이미지경로\" (@ 파일 참조, --print 모드)\n * - codex: exec -i 이미지경로 \"프롬프트\" (-i/--image 플래그)\n * - ollama: REST API (localhost:11434) — CLI는 이미지 입력 미지원\n */\n\nimport { spawnSync } from \"child_process\"\nimport { writeFileSync, readFileSync, unlinkSync, mkdirSync } from \"fs\"\nimport { join } from \"path\"\nimport { tmpdir } from \"os\"\nimport type { OcrMode, StructuredOcrResult } from \"../types.js\"\n\n/** OCR 프롬프트 — 모든 CLI 공통 */\nconst OCR_PROMPT = `이 PDF 페이지 이미지에서 텍스트와 표를 추출하여 순수 Markdown으로 변환하고, 명백한 OCR 오인식과 표 구조 깨짐만 교정하여 최종 결과를 출력하세요.\n\n[기본 추출 규칙]\n- 본문, 표, 제목, 리스트, 캡션을 원문 구조 그대로 Markdown으로 변환\n- 헤딩은 시각적 크기·굵기에 따라 # ~ ###### 사용\n- 리스트는 -, 1. 사용 (원문 번호 체계가 ①, 가., 1) 등이면 그 표기 유지)\n- 이미지·도형·로고·페이지번호·머리글/바닥글은 무시\n- 원문의 읽기 순서(좌→우, 위→아래, 다단이면 단별로)를 유지\n\n[표 추출 규칙 — 가장 중요]\n- 표는 반드시 Markdown 파이프 테이블 문법 사용: 헤더 행 + |---| 구분선 + 데이터 행\n- 시각적으로 굵거나 음영(회색/색상) 배경이 있는 행을 헤더로 식별. 헤더가 없는 표라도 첫 행을 헤더로 두고 |---| 구분선 추가\n- 모든 행의 파이프(|) 개수를 헤더와 동일하게 맞출 것 — 빈 셀은 공백으로 채우고 절대 셀을 생략하지 말 것\n- 셀 안의 줄바꿈은 <br>로 표기 (실제 개행 문자 사용 금지 — 표가 깨짐)\n- 병합 셀 처리:\n - 가로 병합(colspan): 병합된 셀 내용을 첫 칸에 쓰고 나머지 칸은 빈 칸으로 둘 것\n - 세로 병합(rowspan): 병합된 모든 행의 해당 칸에 동일한 내용을 반복 기재할 것\n- 2단 헤더(헤더가 두 줄인 경우): 상위/하위 헤더를 \" / \"로 합쳐 단일 헤더 행으로 만들 것 (예: \"2024 / 1분기\")\n- 표 위·아래의 캡션(예: \"<표 1> ...\", \"[표 2-1]\")은 표 바로 위에 별도 줄로 보존\n- 한 페이지에서 표가 중간에 끊어져 보이더라도, 헤더가 동일하면 하나의 표로 이어붙일 것\n- 좌측 첫 컬럼이 세로쓰기 라벨(구분/분류/연도 등)이라도 일반 셀로 변환하여 누락 금지\n\n[OCR 오인식·깨짐 교정 — 허용 범위만]\n- 숫자 칸에서 'O'→'0', 'l/I'→'1' 등 맥락상 명백한 오인식만 교정\n- 단어 중간에 잘못 삽입된 공백 제거 (예: \"주거 종 합 계획\" → \"주거종합계획\")\n- 줄바꿈 오류로 끊긴 문장은 의미 단위로 병합 (단, 표 셀 내부는 <br> 유지)\n- 표에서 행 어긋남이 의심되면(컬럼 수 불일치) 빈 셀로 패딩하여 컬럼 정합성을 우선 확보\n\n[절대 금지 사항]\n- 문장·단락·항목·표 행/열을 추가하거나 삭제하지 말 것\n- 숫자, 퍼센트, 날짜, 단위, 금액을 절대 변경하지 말 것\n- 고유명사, 기관명, 법령명, 지명, 인명을 변경하지 말 것\n- 표의 헤더 텍스트, 캡션, 행/열 개수를 임의로 바꾸지 말 것\n- 체크박스(☑/☐/■/□), 특수문자(※, ①②③, ㉠, 가., 나.)를 임의로 바꾸지 말 것\n- 원문에 없는 내용을 요약·보완·추론하지 말 것\n- \\`\\`\\`, \\`\\`\\`markdown 같은 코드펜스로 감싸지 말 것\n- 설명·주석·메타 문장 추가 금지\n\n[불확실할 때]\n- 글자가 흐릿하거나 판독 불가하면 보이는 그대로 두거나, 불가피하면 한 글자만 ?로 표시\n- 표 구조가 모호하면 컬럼 수 일치 + 빈 셀 패딩 형태로 출력\n- 추측보다 원문 보존을 항상 우선`\n\n/** 임시 디렉토리 (프로세스당 1회 생성)\n *\n * gemini CLI는 /tmp/ 등 시스템 임시 디렉토리를 워크스페이스 외부로 간주하여\n * @파일참조 시 접근을 거부할 수 있음. cwd 하위 폴더를 사용하면 모든 CLI에서 접근 가능.\n *\n * ⚠️ .gitignore에 포함된 경로(예: .kordoc-tmp/)는 gemini CLI가 무시하므로\n * 반드시 gitignore되지 않는 이름 사용. 파일은 try/finally로 즉시 정리.\n */\nlet _tempDir: string | null = null\nfunction getTempDir(): string {\n if (!_tempDir) {\n _tempDir = join(process.cwd(), \".kordoc_ocr_tmp\")\n mkdirSync(_tempDir, { recursive: true })\n }\n return _tempDir\n}\n\n/**\n * CLI OcrProvider 생성.\n *\n * @param mode - 사용할 CLI (gemini, claude, codex, ollama)\n * @returns OcrProvider 함수 (StructuredOcrResult 반환)\n */\nexport function createCliOcrProvider(\n mode: Exclude<OcrMode, \"auto\" | \"off\">\n): (pageImage: Uint8Array, pageNumber: number, mimeType: \"image/png\") => Promise<StructuredOcrResult> {\n return async (pageImage: Uint8Array, pageNumber: number): Promise<StructuredOcrResult> => {\n const tempPath = join(getTempDir(), `page-${pageNumber}.png`)\n\n try {\n writeFileSync(tempPath, pageImage)\n\n let output: string\n if (mode === \"ollama\") {\n output = await callOllamaApi(tempPath)\n } else {\n output = callCli(mode, tempPath)\n }\n\n return { markdown: stripCodeFence(output.trim()) }\n } finally {\n try { unlinkSync(tempPath) } catch { /* 임시 파일 정리 실패 무시 */ }\n }\n }\n}\n\n/**\n * 출력 텍스트에서 사용량·속도 제한 에러 감지.\n * 해당 메시지가 포함된 경우 throw하여 다음 엔진으로 fallback 트리거.\n */\nfunction checkForLimitError(output: string, mode: string): void {\n const lower = output.toLowerCase()\n if (lower.includes(\"usage limit\") || lower.includes(\"rate limit\")) {\n throw new Error(`${mode} 사용량/속도 제한: ${output.trim().slice(0, 200)}`)\n }\n}\n\n/**\n * CLI 실행 — gemini / claude / codex\n *\n * @throws CLI 실행 실패 또는 타임아웃(180초) 시 Error\n */\nfunction callCli(mode: string, imagePath: string): string {\n // codex는 --output-last-message로 대화 헤더 없는 깔끔한 출력 사용\n if (mode === \"codex\") {\n return callCodexCli(imagePath)\n }\n\n const args = buildCliArgs(mode, imagePath)\n\n const result = spawnSync(mode, args, {\n encoding: \"utf-8\",\n timeout: 600_000,\n maxBuffer: 10 * 1024 * 1024,\n shell: process.platform === \"win32\",\n // claude: /tmp에서 실행하여 프로젝트 CLAUDE.md의 규칙 간섭 방지\n ...(mode === \"claude\" ? { cwd: tmpdir() } : {}),\n })\n\n if (result.error) {\n throw new Error(`${mode} CLI 실행 실패: ${result.error.message}`)\n }\n if (result.status !== 0) {\n const errMsg = result.stderr?.trim() || `exit code ${result.status}`\n throw new Error(`${mode} OCR 실패: ${errMsg}`)\n }\n\n const output = result.stdout || \"\"\n checkForLimitError(output, mode)\n return output\n}\n\n/**\n * codex exec 실행 — --output-last-message로 대화 헤더 없는 깔끔한 출력.\n * 인자 순서: `codex exec <prompt> --image <file> --output-last-message <outfile>`\n */\nfunction callCodexCli(imagePath: string): string {\n // 출력 파일은 /tmp/ 사용 — codex sandbox는 cwd 내 쓰기를 막을 수 있음\n const outPath = join(tmpdir(), `kordoc-codex-out-${Date.now()}.txt`)\n try {\n const args = [\"exec\", OCR_PROMPT, \"--image\", imagePath, \"--output-last-message\", outPath]\n const model = process.env.KORDOC_CODEX_MODEL\n if (model) args.push(\"--model\", model)\n\n const result = spawnSync(\"codex\", args, {\n encoding: \"utf-8\",\n timeout: 180_000,\n maxBuffer: 10 * 1024 * 1024,\n input: \"\", // stdin EOF 즉시 전달 (대화형 입력 차단)\n shell: process.platform === \"win32\",\n })\n\n if (result.error) {\n throw new Error(`codex CLI 실행 실패: ${result.error.message}`)\n }\n if (result.status !== 0) {\n const errMsg = result.stderr?.trim() || `exit code ${result.status}`\n throw new Error(`codex OCR 실패: ${errMsg}`)\n }\n\n // --output-last-message 파일에서 읽기 (없으면 stdout 폴백)\n let text: string\n try {\n text = readFileSync(outPath, \"utf-8\")\n } catch {\n text = result.stdout || \"\"\n }\n checkForLimitError(text, \"codex\")\n return text\n } finally {\n try { unlinkSync(outPath) } catch { /* 무시 */ }\n }\n}\n\n/**\n * CLI별 인자 배열 생성.\n *\n * gemini: [\"--prompt\", \"프롬프트 @이미지경로\", \"--yolo\"]\n * - -y/--yolo: 자동 승인 (OCR은 도구 사용 없으므로 실질적 영향 없음)\n * - @ 파일 참조로 이미지를 컨텍스트에 포함\n *\n * claude: [\"--print\", \"프롬프트 @이미지경로\"]\n * - --print(-p): 비대화형 출력 모드\n * - @ 파일 참조로 이미지를 컨텍스트에 포함\n *\n * codex: callCodexCli()에서 별도 처리\n * - `codex exec <prompt> --image <file> --output-last-message <outfile>`\n * - 프롬프트가 --image보다 앞에 위치해야 함 (인자 순서 중요)\n *\n * ⚠️ CLI 버전에 따라 문법이 다를 수 있음. 업데이트 시 --help 재확인 필요.\n */\nfunction buildCliArgs(mode: string, imagePath: string): string[] {\n const normalizedPath = imagePath.replace(/\\\\/g, \"/\")\n const promptWithImage = `${OCR_PROMPT}\n\n이미지: @${normalizedPath}`\n\n switch (mode) {\n case \"gemini\": {\n const args = [\"--prompt\", promptWithImage, \"--yolo\"]\n const model = process.env.KORDOC_GEMINI_MODEL\n if (model) args.push(\"--model\", model)\n return args\n }\n\n case \"claude\": {\n const args = [\"--print\", promptWithImage]\n const model = process.env.KORDOC_CLAUDE_MODEL\n if (model) args.push(\"--model\", model)\n return args\n }\n\n default:\n throw new Error(`지원하지 않는 CLI: ${mode}`)\n }\n}\n\n/**\n * Ollama REST API 호출 — CLI는 이미지 입력을 지원하지 않으므로 API 직접 사용.\n *\n * 기본 모델: KORDOC_OLLAMA_MODEL 환경변수 또는 \"gemma4:27b\"\n * 기본 호스트: KORDOC_OLLAMA_HOST 환경변수 또는 \"http://localhost:11434\"\n *\n * @throws Ollama 서버 미실행 또는 응답 오류 시 Error\n */\nasync function callOllamaApi(imagePath: string): Promise<string> {\n const { readFileSync } = await import(\"fs\")\n const imageBase64 = readFileSync(imagePath).toString(\"base64\")\n\n const model = process.env.KORDOC_OLLAMA_MODEL || \"qwen3-vl:8b\"\n const host = process.env.KORDOC_OLLAMA_HOST || \"http://localhost:11434\"\n const timeoutMs = Number(process.env.KORDOC_OLLAMA_TIMEOUT) || 120_000\n\n const response = await fetch(`${host}/api/chat`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model,\n messages: [{\n role: \"user\",\n content: OCR_PROMPT,\n images: [imageBase64],\n }],\n stream: false,\n }),\n signal: AbortSignal.timeout(timeoutMs),\n })\n\n if (!response.ok) {\n throw new Error(`Ollama API 오류: ${response.status} ${response.statusText}`)\n }\n\n const data = await response.json() as { message?: { content?: string } }\n return data.message?.content || \"\"\n}\n\n/**\n * LLM 출력에서 코드 펜스 제거.\n * LLM이 가끔 결과를 ```markdown ... ``` 으로 감싸는 경우 처리.\n */\nfunction stripCodeFence(text: string): string {\n const match = text.match(/^```(?:markdown|md)?\\s*([\\s\\S]*?)```\\s*$/m)\n return match ? match[1].trim() : text\n}\n"],"mappings":"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","names":["readFileSync"]}
|
package/dist/cli.js
CHANGED
|
@@ -4,16 +4,13 @@ import {
|
|
|
4
4
|
markdownToHwpx,
|
|
5
5
|
markdownToXlsx,
|
|
6
6
|
parse
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-CPTOBJJD.js";
|
|
9
|
-
import "./chunk-THBLCND6.js";
|
|
10
|
-
import "./chunk-AEWWERJ5.js";
|
|
7
|
+
} from "./chunk-25ZYYLVP.js";
|
|
11
8
|
import {
|
|
12
9
|
VERSION,
|
|
13
10
|
toArrayBuffer
|
|
14
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-5CILZHRW.js";
|
|
15
12
|
import "./chunk-MOL7MDBG.js";
|
|
16
|
-
import "./chunk-
|
|
13
|
+
import "./chunk-S7BHLD2V.js";
|
|
17
14
|
import "./chunk-YW5G6BCJ.js";
|
|
18
15
|
import {
|
|
19
16
|
createLoggerFromEnv,
|
|
@@ -176,7 +173,7 @@ async function runParse(files, opts) {
|
|
|
176
173
|
saveImages(absPath);
|
|
177
174
|
}
|
|
178
175
|
} catch (err) {
|
|
179
|
-
const { sanitizeError } = await import("./utils-
|
|
176
|
+
const { sanitizeError } = await import("./utils-H2BL5GNR.js");
|
|
180
177
|
process.stderr.write(`
|
|
181
178
|
[kordoc] ERROR: ${fileName} \u2014 ${sanitizeError(err)}
|
|
182
179
|
`);
|
|
@@ -258,7 +255,7 @@ program.command("convert <input>").description("\uB9C8\uD06C\uB2E4\uC6B4 \uD30C\
|
|
|
258
255
|
`));
|
|
259
256
|
}
|
|
260
257
|
} catch (err) {
|
|
261
|
-
const { sanitizeError } = await import("./utils-
|
|
258
|
+
const { sanitizeError } = await import("./utils-H2BL5GNR.js");
|
|
262
259
|
process.stderr.write(` FAIL
|
|
263
260
|
`);
|
|
264
261
|
process.stderr.write(` \u2192 ${sanitizeError(err)}
|
|
@@ -290,7 +287,7 @@ program.command("init-env").description("kordoc\uC6A9 .env \uD15C\uD50C\uB9BF \u
|
|
|
290
287
|
}
|
|
291
288
|
});
|
|
292
289
|
program.command("watch <dir>").description("\uB514\uB809\uD1A0\uB9AC \uAC10\uC2DC \u2014 \uC0C8 \uBB38\uC11C \uC790\uB3D9 \uBCC0\uD658").option("--webhook <url>", "\uACB0\uACFC \uC804\uC1A1 \uC6F9\uD6C5 URL").option("-d, --out-dir <dir>", "\uBCC0\uD658 \uACB0\uACFC \uCD9C\uB825 \uB514\uB809\uD1A0\uB9AC").option("-p, --pages <range>", "\uD398\uC774\uC9C0/\uC139\uC158 \uBC94\uC704").option("--format <type>", "\uCD9C\uB825 \uD615\uC2DD: markdown \uB610\uB294 json", "markdown").option("--silent", "\uC9C4\uD589 \uBA54\uC2DC\uC9C0 \uC228\uAE30\uAE30").action(async (dir, opts) => {
|
|
293
|
-
const { watchDirectory } = await import("./watch-
|
|
290
|
+
const { watchDirectory } = await import("./watch-D6ODQLPJ.js");
|
|
294
291
|
await watchDirectory({
|
|
295
292
|
dir,
|
|
296
293
|
outDir: opts.outDir,
|
|
@@ -300,35 +297,6 @@ program.command("watch <dir>").description("\uB514\uB809\uD1A0\uB9AC \uAC10\uC2D
|
|
|
300
297
|
silent: opts.silent
|
|
301
298
|
});
|
|
302
299
|
});
|
|
303
|
-
program.command("doctor").description("OCR \uD30C\uC774\uD504\uB77C\uC778 \uC758\uC874\uC131 \uC810\uAC80 (poppler, LibreOffice)").action(() => {
|
|
304
|
-
import("./doctor-SJ7NYSXC.js").then(({ runDoctor }) => {
|
|
305
|
-
const result = runDoctor();
|
|
306
|
-
process.exitCode = result.ok ? 0 : 1;
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
program.command("setup").description("OCR \uC758\uC874\uC131 \uC124\uCE58 \uC548\uB0B4 \uBC0F \uC810\uAC80").option("--check", "\uC810\uAC80\uB9CC \uC218\uD589 (doctor\uC640 \uB3D9\uC77C)").option("--print", "\uC124\uCE58 \uBA85\uB839\uC5B4\uB9CC \uCD9C\uB825").option("--non-interactive", "CI \uBAA8\uB4DC \u2014 \uB300\uD654 \uC5C6\uC774 \uC9C4\uD589, \uACB0\uC190 \uC2DC exit 1").action((opts) => {
|
|
310
|
-
import("./doctor-SJ7NYSXC.js").then(({ runDoctor }) => {
|
|
311
|
-
const result = runDoctor();
|
|
312
|
-
if (opts.print) {
|
|
313
|
-
import("./install-commands-P2KTEXQ4.js").then(({ POPPLER_RECIPES, LIBREOFFICE_RECIPES }) => {
|
|
314
|
-
import("./pm-7KGLH6MX.js").then(({ detectPackageManager }) => {
|
|
315
|
-
const pm = detectPackageManager();
|
|
316
|
-
if (result.missing.includes("poppler")) {
|
|
317
|
-
const r = POPPLER_RECIPES[pm];
|
|
318
|
-
if (r) process.stdout.write(`${r.cmd}
|
|
319
|
-
`);
|
|
320
|
-
}
|
|
321
|
-
if (result.missing.includes("libreoffice")) {
|
|
322
|
-
const r = LIBREOFFICE_RECIPES[pm];
|
|
323
|
-
if (r) process.stdout.write(`${r.cmd}
|
|
324
|
-
`);
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
process.exitCode = result.ok ? 0 : 1;
|
|
330
|
-
});
|
|
331
|
-
});
|
|
332
300
|
addParseOptions(
|
|
333
301
|
program.argument("<files...>", "\uBCC0\uD658\uD560 \uD30C\uC77C \uACBD\uB85C (HWP, HWPX, PDF, XLSX, DOCX)")
|
|
334
302
|
).action(runParse);
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["/** kordoc CLI — 모두 파싱해버리겠다 */\n\nimport { readFileSync, writeFileSync, mkdirSync, statSync, existsSync, readdirSync } from \"fs\"\nimport { basename, resolve, extname } from \"path\"\nimport { cpus } from \"os\"\nimport { Command } from \"commander\"\nimport { parse, detectFormat, markdownToHwpx, markdownToXlsx } from \"./index.js\"\nimport type { ParseOptions, OcrMode } from \"./types.js\"\nimport { VERSION, toArrayBuffer } from \"./utils.js\"\nimport { createLoggerFromEnv, generateRunId } from \"./logging/logger.js\"\n\nconst program = new Command()\n\nfunction buildEnvTemplate(): string {\n return [\n \"# kordoc 환경 변수 템플릿\",\n \"# 필요 항목만 주석 해제해서 사용하세요.\",\n \"\",\n \"# ---------- OCR ----------\",\n \"# CLI OCR 모드에서 Ollama 모델 지정\",\n \"# KORDOC_OLLAMA_MODEL=qwen3-vl:8b\",\n \"\",\n \"# Unified OCR 파이프라인 모델 기본값\",\n \"KORDOC_UNIFIED_MODEL_CANDIDATES=mistralai/mistral-medium-3-instruct,moonshotai/kimi-k2.5,qwen/qwen3.5-122b-a10b,qwen/qwen3.5-397b-a17b,moonshotai/kimi-k2-thinking,moonshotai/kimi-k2-instruct,moonshotai/kimi-k2-instruct-0905\",\n \"KORDOC_UNIFIED_MODEL_MAX_TOKENS={\\\"mistralai/mistral-medium-3-instruct\\\":8192,\\\"moonshotai/kimi-k2.5\\\":64000,\\\"qwen/qwen3.5-122b-a10b\\\":64000,\\\"qwen/qwen3.5-397b-a17b\\\":64000,\\\"moonshotai/kimi-k2-thinking\\\":64000,\\\"moonshotai/kimi-k2-instruct\\\":64000,\\\"moonshotai/kimi-k2-instruct-0905\\\":64000}\",\n \"\",\n \"# NIM API 키 (단일/다중)\",\n \"NVIDIA_API_KEY=\",\n \"# NVIDIA_API_KEYS=nvapi-key1,nvapi-key2,nvapi-key3\",\n \"\",\n \"# ---------- 로깅 ----------\",\n \"# KORDOC_LOG_LEVEL=error\",\n \"# KORDOC_LOG_FILE=./logs/kordoc.jsonl\",\n \"# KORDOC_LOG_STACK=0\",\n \"# KORDOC_LOG_PROGRESS_SAMPLE_MS=1000\",\n \"# KORDOC_LOG_BASENAME_PATHS=1\",\n \"# KORDOC_LOG_TEXT_LIMIT=400\",\n \"\",\n ].join(\"\\n\")\n}\n\n/** 공통 parse 옵션 타입 */\ninterface ParseOpts {\n output?: string\n outDir?: string\n pages?: string\n format: string\n headerFooter: boolean\n imageDir?: string\n silent?: boolean\n ocr?: string\n ocrJobs?: string\n ocrBatchSize?: string\n}\n\n/** parse 액션 공통 구현 — 루트 커맨드와 `parse` 서브커맨드가 공유 */\nasync function runParse(files: string[], opts: ParseOpts) {\n const logger = createLoggerFromEnv().withRun(generateRunId(\"cli\")).child({ component: \"cli.ts\" })\n logger.log({ level: \"info\", stage: \"detect\", event: \"start\", message: \"CLI parse 시작\", meta: { files: files.length } })\n const validFormats = [\"markdown\", \"json\"]\n if (!validFormats.includes(opts.format)) {\n process.stderr.write(`[kordoc] 지원하지 않는 형식: ${opts.format} (markdown 또는 json)\\n`)\n process.exit(1)\n }\n for (let fi = 0; fi < files.length; fi++) {\n const filePath = files[fi]\n const absPath = resolve(filePath)\n const fileName = basename(absPath)\n const filePrefix = files.length > 1 ? `[${fi + 1}/${files.length}] ` : \"\"\n\n try {\n const fileSize = statSync(absPath).size\n if (fileSize > 500 * 1024 * 1024) {\n process.stderr.write(`\\n[kordoc] SKIP: ${fileName} — 파일이 너무 큽니다 (${(fileSize / 1024 / 1024).toFixed(1)}MB)\\n`)\n process.exitCode = 1\n continue\n }\n const buffer = readFileSync(absPath)\n const arrayBuffer = toArrayBuffer(buffer)\n const format = detectFormat(arrayBuffer)\n\n if (!opts.silent) {\n process.stderr.write(`[kordoc] ${filePrefix}${fileName} (${format}) ...`)\n }\n logger.log({ level: \"debug\", stage: \"detect\", event: \"done\", message: \"파일 포맷 감지\", meta: { fileName, format } })\n\n const parseOptions: ParseOptions = {}\n if (opts.pages) parseOptions.pages = opts.pages as string\n if (opts.headerFooter === false) parseOptions.removeHeaderFooter = false\n\n // OCR 모드: CLI 기본값 \"auto\" (라이브러리 API는 undefined 유지)\n const validOcrModes = [\"auto\", \"gemini\", \"claude\", \"codex\", \"ollama\", \"off\"]\n if (opts.ocr) {\n if (!validOcrModes.includes(opts.ocr)) {\n process.stderr.write(`[kordoc] 지원하지 않는 OCR 모드: ${opts.ocr}\\n`)\n process.stderr.write(` 사용 가능: ${validOcrModes.join(\", \")}\\n`)\n process.exit(1)\n }\n parseOptions.ocrMode = opts.ocr as OcrMode\n } else {\n parseOptions.ocrMode = \"auto\"\n }\n\n // OCR 병렬 처리 수 (--ocr-jobs)\n if (opts.ocrJobs) {\n const n = parseInt(opts.ocrJobs, 10)\n if (n > 0) parseOptions.ocrConcurrency = n\n } else {\n parseOptions.ocrConcurrency = 4\n }\n\n // OCR 배치 크기 (--ocr-batch-size)\n if (opts.ocrBatchSize) {\n const n = parseInt(opts.ocrBatchSize, 10)\n if (n > 0) parseOptions.ocrBatchSize = n\n }\n\n if (!opts.silent) {\n parseOptions.onProgress = (current: number, total: number) => {\n const pct = total > 0 ? Math.round((current / total) * 100) : 0\n const filled = Math.round(pct / 5) // 20칸 진행바\n const bar = \"█\".repeat(filled) + \"░\".repeat(20 - filled)\n const cr = process.stderr.isTTY ? \"\\r\" : \"\\n\"\n process.stderr.write(`${cr}[kordoc] ${filePrefix}${fileName} — OCR: [${bar}] ${pct}% (${current}/${total}페이지)`)\n }\n }\n const result = await parse(arrayBuffer, parseOptions)\n\n if (!result.success) {\n process.stderr.write(` FAIL\\n`)\n process.stderr.write(` → ${result.error}\\n`)\n logger.log({ level: \"error\", stage: \"finalize\", event: \"error\", message: \"파일 파싱 실패\", meta: { fileName, error: result.error, code: result.code } })\n process.exitCode = 1\n continue\n }\n\n if (!opts.silent) process.stderr.write(` OK\\n`)\n logger.log({ level: \"info\", stage: \"finalize\", event: \"done\", message: \"파일 파싱 완료\", meta: { fileName, output: opts.output ?? opts.outDir ?? \"stdout\" } })\n\n // 이미지 기반 PDF OCR 결과 표시\n if (!opts.silent && result.success && result.isImageBased) {\n process.stderr.write(` → 이미지 기반 PDF — OCR 처리됨\\n`)\n }\n\n // 경고 표시\n if (!opts.silent && result.success && result.warnings?.length) {\n for (const w of result.warnings) {\n process.stderr.write(` ⚠ ${w.message}\\n`)\n }\n }\n\n let markdown = result.markdown\n // --out-dir 시 이미지 참조 경로에 images/ 접두사 추가\n if (opts.outDir && result.images?.length) {\n markdown = markdown.replace(/!\\[image\\]\\(image_/g, \"\n }\n const output = opts.format === \"json\"\n ? JSON.stringify(result, null, 2)\n : markdown\n\n // 이미지 저장: 출력 MD 파일 기준 폴더 사용 (convert와 일치)\n const saveImages = (outFilePath: string) => {\n if (!result.images?.length) return\n const stem = basename(outFilePath).replace(/\\.[^.]+$/, \"\")\n const defaultDir = resolve(outFilePath, \"..\", stem + \"_images\")\n const imgDir = opts.imageDir ? resolve(opts.imageDir) : defaultDir\n mkdirSync(imgDir, { recursive: true })\n for (const img of result.images) {\n writeFileSync(resolve(imgDir, img.filename), img.data)\n }\n if (!opts.silent) process.stderr.write(` → ${result.images.length}개 이미지 → ${imgDir}\\n`)\n }\n\n if (opts.output && files.length === 1) {\n writeFileSync(opts.output, output, \"utf-8\")\n if (!opts.silent) process.stderr.write(` → ${opts.output}\\n`)\n saveImages(resolve(opts.output))\n } else if (opts.outDir) {\n mkdirSync(opts.outDir, { recursive: true })\n const outExt = opts.format === \"json\" ? \".json\" : \".md\"\n const outPath = resolve(opts.outDir, fileName.replace(/\\.[^.]+$/, outExt))\n writeFileSync(outPath, output, \"utf-8\")\n if (!opts.silent) process.stderr.write(` → ${outPath}\\n`)\n saveImages(outPath)\n } else {\n process.stdout.write(output + \"\\n\")\n saveImages(absPath) // stdout 출력 시 입력 파일 기준\n }\n } catch (err) {\n const { sanitizeError } = await import(\"./utils.js\")\n process.stderr.write(`\\n[kordoc] ERROR: ${fileName} — ${sanitizeError(err)}\\n`)\n logger.log({ level: \"error\", stage: \"finalize\", event: \"error\", message: \"CLI 처리 중 예외\", error: { message: sanitizeError(err), name: err instanceof Error ? err.name : \"Error\", stack: err instanceof Error ? err.stack : undefined } })\n process.exitCode = 1\n }\n }\n}\n\n/** 공통 parse 옵션 등록 헬퍼 */\nfunction addParseOptions(cmd: Command): Command {\n return cmd\n .option(\"-o, --output <path>\", \"출력 파일 경로 (단일 파일 시)\")\n .option(\"-d, --out-dir <dir>\", \"출력 디렉토리 (다중 파일 시)\")\n .option(\"-p, --pages <range>\", \"페이지/섹션 범위 (예: 1-3, 1,3,5)\")\n .option(\"--format <type>\", \"출력 형식: markdown (기본) 또는 json\", \"markdown\")\n .option(\"--no-header-footer\", \"PDF 머리글/바닥글 자동 제거\")\n .option(\"--image-dir <dir>\", \"이미지 저장 폴더 (기본: 입력 파일명_images 폴더)\")\n .option(\"--silent\", \"진행 메시지 숨기기\")\n .option(\"--ocr <mode>\", \"OCR 모드: auto(기본), gemini, claude, codex, ollama, off\")\n .option(\"--ocr-jobs <n>\", \"OCR 병렬 처리 수 (기본: 4)\")\n .option(\"--ocr-batch-size <n>\", \"OCR 배치 크기 — CLI당 페이지 수 (기본: gemini/claude 50, codex 100)\")\n}\n\nprogram\n .enablePositionalOptions()\n .name(\"kordoc\")\n .description(\"모두 파싱해버리겠다 — HWP, HWPX, PDF, XLSX, DOCX → Markdown\")\n .version(VERSION)\n\n// `kordoc parse <files>` 서브커맨드 (권장)\naddParseOptions(\n program\n .command(\"parse\")\n .description(\"파일을 마크다운으로 파싱 (HWP, HWPX, PDF, XLSX, DOCX)\")\n .argument(\"<files...>\", \"변환할 파일 경로\")\n).action(runParse)\n\nprogram\n .command(\"convert <input>\")\n .description(\"마크다운 파일을 HWPX 또는 XLSX로 변환\")\n .option(\"-f, --format <type>\", \"출력 포맷: hwpx | xlsx\", \"hwpx\")\n .option(\"-o, --output <path>\", \"출력 파일 경로 (기본: 입력명 + 포맷 확장자)\")\n .option(\"--image-dir <dir>\", \"이미지 폴더 경로 (기본: 입력 MD 파일명_images 폴더)\")\n .option(\"--images\", \"이미지 포함 (기본: 생략, 레이아웃 문제 방지)\")\n .option(\"--template <path>\", \"HWPX 템플릿 파일 경로 (hwpx 전용)\")\n .option(\"--silent\", \"진행 메시지 숨기기\")\n .action(async (input: string, opts) => {\n const validFormats = [\"hwpx\", \"xlsx\"]\n if (!validFormats.includes(opts.format)) {\n process.stderr.write(`[kordoc] 지원하지 않는 포맷: ${opts.format} (hwpx 또는 xlsx)\\n`)\n process.exit(1)\n }\n\n const absInput = resolve(input)\n if (!existsSync(absInput)) {\n process.stderr.write(`[kordoc] 파일을 찾을 수 없습니다: ${input}\\n`)\n process.exit(1)\n }\n\n const stem = basename(absInput).replace(/\\.[^.]+$/, \"\")\n const outPath = opts.output\n ? resolve(opts.output)\n : resolve(absInput, \"..\", `${stem}.${opts.format}`)\n\n if (!opts.silent) process.stderr.write(`[kordoc] ${basename(absInput)} → ${basename(outPath)} ...`)\n\n try {\n const markdown = readFileSync(absInput, \"utf-8\")\n\n // 이미지 폴더에서 이미지 로드 (--images 플래그 필요)\n const imgDir = opts.imageDir ? resolve(opts.imageDir) : resolve(absInput, \"..\", stem + \"_images\")\n const images: import(\"./types.js\").ExtractedImage[] = []\n if (opts.images && existsSync(imgDir)) {\n const mimeMap: Record<string, string> = {\n png: \"image/png\", jpg: \"image/jpeg\", jpeg: \"image/jpeg\",\n gif: \"image/gif\", bmp: \"image/bmp\",\n }\n for (const entry of readdirSync(imgDir, { withFileTypes: true })) {\n if (!entry.isFile()) continue\n const fname = entry.name\n const ext = extname(fname).slice(1).toLowerCase()\n if (!mimeMap[ext]) continue\n const data = readFileSync(resolve(imgDir, fname))\n images.push({ filename: fname, data: new Uint8Array(data), mimeType: mimeMap[ext] })\n }\n if (!opts.silent) process.stderr.write(` → 이미지 ${images.length}개 로드\\n`)\n }\n\n const warnings: string[] = []\n\n let buf: ArrayBuffer\n if (opts.format === \"xlsx\") {\n if (opts.template && !opts.silent) {\n process.stderr.write(`\\n[kordoc] 경고: --template은 hwpx 전용입니다. 무시됩니다.\\n`)\n }\n buf = await markdownToXlsx(markdown, { warnings, images: images.length ? images : undefined })\n } else {\n let templateArrayBuffer: ArrayBuffer | undefined\n if (opts.template) {\n const tmplBuf = readFileSync(resolve(opts.template))\n templateArrayBuffer = tmplBuf.buffer.slice(tmplBuf.byteOffset, tmplBuf.byteOffset + tmplBuf.byteLength)\n }\n buf = await markdownToHwpx(markdown, { warnings, images: images.length ? images : undefined, templateArrayBuffer })\n }\n\n writeFileSync(outPath, Buffer.from(buf))\n\n if (!opts.silent) {\n process.stderr.write(` OK\\n`)\n process.stderr.write(` → ${outPath}\\n`)\n if (warnings.length) warnings.forEach(w => process.stderr.write(` ${w}\\n`))\n }\n } catch (err) {\n const { sanitizeError } = await import(\"./utils.js\")\n process.stderr.write(` FAIL\\n`)\n process.stderr.write(` → ${sanitizeError(err)}\\n`)\n process.exit(1)\n }\n })\n\nprogram\n .command(\"init-env\")\n .description(\"kordoc용 .env 템플릿 생성 (명시 실행 시에만 파일 생성)\")\n .option(\"-o, --output <path>\", \"템플릿 출력 경로\", \".env.kordoc.example\")\n .option(\"--create-dotenv\", \".env 파일이 없으면 함께 생성\")\n .option(\"--force\", \"기존 파일이 있어도 덮어쓰기\")\n .action((opts: { output: string; createDotenv?: boolean; force?: boolean }) => {\n const template = buildEnvTemplate()\n const outPath = resolve(opts.output)\n\n if (existsSync(outPath) && !opts.force) {\n process.stderr.write(`[kordoc] 이미 파일이 있습니다: ${outPath} (덮어쓰려면 --force)\\n`)\n process.exit(1)\n }\n writeFileSync(outPath, template, \"utf-8\")\n process.stderr.write(`[kordoc] 생성됨: ${outPath}\\n`)\n\n if (opts.createDotenv) {\n const dotenvPath = resolve(\".env\")\n if (existsSync(dotenvPath) && !opts.force) {\n process.stderr.write(`[kordoc] .env가 이미 있어 생성하지 않았습니다: ${dotenvPath}\\n`)\n } else {\n writeFileSync(dotenvPath, template, \"utf-8\")\n process.stderr.write(`[kordoc] 생성됨: ${dotenvPath}\\n`)\n }\n }\n })\n\nprogram\n .command(\"watch <dir>\")\n .description(\"디렉토리 감시 — 새 문서 자동 변환\")\n .option(\"--webhook <url>\", \"결과 전송 웹훅 URL\")\n .option(\"-d, --out-dir <dir>\", \"변환 결과 출력 디렉토리\")\n .option(\"-p, --pages <range>\", \"페이지/섹션 범위\")\n .option(\"--format <type>\", \"출력 형식: markdown 또는 json\", \"markdown\")\n .option(\"--silent\", \"진행 메시지 숨기기\")\n .action(async (dir: string, opts) => {\n const { watchDirectory } = await import(\"./watch.js\")\n await watchDirectory({\n dir,\n outDir: opts.outDir,\n webhook: opts.webhook,\n format: opts.format,\n pages: opts.pages,\n silent: opts.silent,\n })\n })\n\nprogram\n .command(\"doctor\")\n .description(\"OCR 파이프라인 의존성 점검 (poppler, LibreOffice)\")\n .action(() => {\n import(\"./setup/doctor.js\").then(({ runDoctor }) => {\n const result = runDoctor()\n process.exitCode = result.ok ? 0 : 1\n })\n })\n\nprogram\n .command(\"setup\")\n .description(\"OCR 의존성 설치 안내 및 점검\")\n .option(\"--check\", \"점검만 수행 (doctor와 동일)\")\n .option(\"--print\", \"설치 명령어만 출력\")\n .option(\"--non-interactive\", \"CI 모드 — 대화 없이 진행, 결손 시 exit 1\")\n .action((opts) => {\n import(\"./setup/doctor.js\").then(({ runDoctor }) => {\n const result = runDoctor()\n if (opts.print) {\n // 설치 명령만 출력\n import(\"./setup/install-commands.js\").then(({ POPPLER_RECIPES, LIBREOFFICE_RECIPES }) => {\n import(\"./setup/pm.js\").then(({ detectPackageManager }) => {\n const pm = detectPackageManager()\n if (result.missing.includes(\"poppler\")) {\n const r = POPPLER_RECIPES[pm]\n if (r) process.stdout.write(`${r.cmd}\\n`)\n }\n if (result.missing.includes(\"libreoffice\")) {\n const r = LIBREOFFICE_RECIPES[pm]\n if (r) process.stdout.write(`${r.cmd}\\n`)\n }\n })\n })\n }\n process.exitCode = result.ok ? 0 : 1\n })\n })\n\n// `kordoc <files>` 루트 커맨드 (하위 호환)\n// 주의: 서브커맨드(init-env 등)보다 뒤에서 등록해야 충돌하지 않음\naddParseOptions(\n program\n .argument(\"<files...>\", \"변환할 파일 경로 (HWP, HWPX, PDF, XLSX, DOCX)\")\n).action(runParse)\n\nprogram.parse()\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["/** kordoc CLI — 모두 파싱해버리겠다 */\n\nimport { readFileSync, writeFileSync, mkdirSync, statSync, existsSync, readdirSync } from \"fs\"\nimport { basename, resolve, extname } from \"path\"\nimport { cpus } from \"os\"\nimport { Command } from \"commander\"\nimport { parse, detectFormat, markdownToHwpx, markdownToXlsx } from \"./index.js\"\nimport type { ParseOptions, OcrMode } from \"./types.js\"\nimport { VERSION, toArrayBuffer } from \"./utils.js\"\nimport { createLoggerFromEnv, generateRunId } from \"./logging/logger.js\"\n\nconst program = new Command()\n\nfunction buildEnvTemplate(): string {\n return [\n \"# kordoc 환경 변수 템플릿\",\n \"# 필요 항목만 주석 해제해서 사용하세요.\",\n \"\",\n \"# ---------- OCR ----------\",\n \"# CLI OCR 모드에서 Ollama 모델 지정\",\n \"# KORDOC_OLLAMA_MODEL=qwen3-vl:8b\",\n \"\",\n \"# Unified OCR 파이프라인 모델 기본값\",\n \"KORDOC_UNIFIED_MODEL_CANDIDATES=mistralai/mistral-medium-3-instruct,moonshotai/kimi-k2.5,qwen/qwen3.5-122b-a10b,qwen/qwen3.5-397b-a17b,moonshotai/kimi-k2-thinking,moonshotai/kimi-k2-instruct,moonshotai/kimi-k2-instruct-0905\",\n \"KORDOC_UNIFIED_MODEL_MAX_TOKENS={\\\"mistralai/mistral-medium-3-instruct\\\":8192,\\\"moonshotai/kimi-k2.5\\\":64000,\\\"qwen/qwen3.5-122b-a10b\\\":64000,\\\"qwen/qwen3.5-397b-a17b\\\":64000,\\\"moonshotai/kimi-k2-thinking\\\":64000,\\\"moonshotai/kimi-k2-instruct\\\":64000,\\\"moonshotai/kimi-k2-instruct-0905\\\":64000}\",\n \"\",\n \"# NIM API 키 (단일/다중)\",\n \"NVIDIA_API_KEY=\",\n \"# NVIDIA_API_KEYS=nvapi-key1,nvapi-key2,nvapi-key3\",\n \"\",\n \"# ---------- 로깅 ----------\",\n \"# KORDOC_LOG_LEVEL=error\",\n \"# KORDOC_LOG_FILE=./logs/kordoc.jsonl\",\n \"# KORDOC_LOG_STACK=0\",\n \"# KORDOC_LOG_PROGRESS_SAMPLE_MS=1000\",\n \"# KORDOC_LOG_BASENAME_PATHS=1\",\n \"# KORDOC_LOG_TEXT_LIMIT=400\",\n \"\",\n ].join(\"\\n\")\n}\n\n/** 공통 parse 옵션 타입 */\ninterface ParseOpts {\n output?: string\n outDir?: string\n pages?: string\n format: string\n headerFooter: boolean\n imageDir?: string\n silent?: boolean\n ocr?: string\n ocrJobs?: string\n ocrBatchSize?: string\n}\n\n/** parse 액션 공통 구현 — 루트 커맨드와 `parse` 서브커맨드가 공유 */\nasync function runParse(files: string[], opts: ParseOpts) {\n const logger = createLoggerFromEnv().withRun(generateRunId(\"cli\")).child({ component: \"cli.ts\" })\n logger.log({ level: \"info\", stage: \"detect\", event: \"start\", message: \"CLI parse 시작\", meta: { files: files.length } })\n const validFormats = [\"markdown\", \"json\"]\n if (!validFormats.includes(opts.format)) {\n process.stderr.write(`[kordoc] 지원하지 않는 형식: ${opts.format} (markdown 또는 json)\\n`)\n process.exit(1)\n }\n for (let fi = 0; fi < files.length; fi++) {\n const filePath = files[fi]\n const absPath = resolve(filePath)\n const fileName = basename(absPath)\n const filePrefix = files.length > 1 ? `[${fi + 1}/${files.length}] ` : \"\"\n\n try {\n const fileSize = statSync(absPath).size\n if (fileSize > 500 * 1024 * 1024) {\n process.stderr.write(`\\n[kordoc] SKIP: ${fileName} — 파일이 너무 큽니다 (${(fileSize / 1024 / 1024).toFixed(1)}MB)\\n`)\n process.exitCode = 1\n continue\n }\n const buffer = readFileSync(absPath)\n const arrayBuffer = toArrayBuffer(buffer)\n const format = detectFormat(arrayBuffer)\n\n if (!opts.silent) {\n process.stderr.write(`[kordoc] ${filePrefix}${fileName} (${format}) ...`)\n }\n logger.log({ level: \"debug\", stage: \"detect\", event: \"done\", message: \"파일 포맷 감지\", meta: { fileName, format } })\n\n const parseOptions: ParseOptions = {}\n if (opts.pages) parseOptions.pages = opts.pages as string\n if (opts.headerFooter === false) parseOptions.removeHeaderFooter = false\n\n // OCR 모드: CLI 기본값 \"auto\" (라이브러리 API는 undefined 유지)\n const validOcrModes = [\"auto\", \"gemini\", \"claude\", \"codex\", \"ollama\", \"off\"]\n if (opts.ocr) {\n if (!validOcrModes.includes(opts.ocr)) {\n process.stderr.write(`[kordoc] 지원하지 않는 OCR 모드: ${opts.ocr}\\n`)\n process.stderr.write(` 사용 가능: ${validOcrModes.join(\", \")}\\n`)\n process.exit(1)\n }\n parseOptions.ocrMode = opts.ocr as OcrMode\n } else {\n parseOptions.ocrMode = \"auto\"\n }\n\n // OCR 병렬 처리 수 (--ocr-jobs)\n if (opts.ocrJobs) {\n const n = parseInt(opts.ocrJobs, 10)\n if (n > 0) parseOptions.ocrConcurrency = n\n } else {\n parseOptions.ocrConcurrency = 4\n }\n\n // OCR 배치 크기 (--ocr-batch-size)\n if (opts.ocrBatchSize) {\n const n = parseInt(opts.ocrBatchSize, 10)\n if (n > 0) parseOptions.ocrBatchSize = n\n }\n\n if (!opts.silent) {\n parseOptions.onProgress = (current: number, total: number) => {\n const pct = total > 0 ? Math.round((current / total) * 100) : 0\n const filled = Math.round(pct / 5) // 20칸 진행바\n const bar = \"█\".repeat(filled) + \"░\".repeat(20 - filled)\n const cr = process.stderr.isTTY ? \"\\r\" : \"\\n\"\n process.stderr.write(`${cr}[kordoc] ${filePrefix}${fileName} — OCR: [${bar}] ${pct}% (${current}/${total}페이지)`)\n }\n }\n const result = await parse(arrayBuffer, parseOptions)\n\n if (!result.success) {\n process.stderr.write(` FAIL\\n`)\n process.stderr.write(` → ${result.error}\\n`)\n logger.log({ level: \"error\", stage: \"finalize\", event: \"error\", message: \"파일 파싱 실패\", meta: { fileName, error: result.error, code: result.code } })\n process.exitCode = 1\n continue\n }\n\n if (!opts.silent) process.stderr.write(` OK\\n`)\n logger.log({ level: \"info\", stage: \"finalize\", event: \"done\", message: \"파일 파싱 완료\", meta: { fileName, output: opts.output ?? opts.outDir ?? \"stdout\" } })\n\n // 이미지 기반 PDF OCR 결과 표시\n if (!opts.silent && result.success && result.isImageBased) {\n process.stderr.write(` → 이미지 기반 PDF — OCR 처리됨\\n`)\n }\n\n // 경고 표시\n if (!opts.silent && result.success && result.warnings?.length) {\n for (const w of result.warnings) {\n process.stderr.write(` ⚠ ${w.message}\\n`)\n }\n }\n\n let markdown = result.markdown\n // --out-dir 시 이미지 참조 경로에 images/ 접두사 추가\n if (opts.outDir && result.images?.length) {\n markdown = markdown.replace(/!\\[image\\]\\(image_/g, \"\n }\n const output = opts.format === \"json\"\n ? JSON.stringify(result, null, 2)\n : markdown\n\n // 이미지 저장: 출력 MD 파일 기준 폴더 사용 (convert와 일치)\n const saveImages = (outFilePath: string) => {\n if (!result.images?.length) return\n const stem = basename(outFilePath).replace(/\\.[^.]+$/, \"\")\n const defaultDir = resolve(outFilePath, \"..\", stem + \"_images\")\n const imgDir = opts.imageDir ? resolve(opts.imageDir) : defaultDir\n mkdirSync(imgDir, { recursive: true })\n for (const img of result.images) {\n writeFileSync(resolve(imgDir, img.filename), img.data)\n }\n if (!opts.silent) process.stderr.write(` → ${result.images.length}개 이미지 → ${imgDir}\\n`)\n }\n\n if (opts.output && files.length === 1) {\n writeFileSync(opts.output, output, \"utf-8\")\n if (!opts.silent) process.stderr.write(` → ${opts.output}\\n`)\n saveImages(resolve(opts.output))\n } else if (opts.outDir) {\n mkdirSync(opts.outDir, { recursive: true })\n const outExt = opts.format === \"json\" ? \".json\" : \".md\"\n const outPath = resolve(opts.outDir, fileName.replace(/\\.[^.]+$/, outExt))\n writeFileSync(outPath, output, \"utf-8\")\n if (!opts.silent) process.stderr.write(` → ${outPath}\\n`)\n saveImages(outPath)\n } else {\n process.stdout.write(output + \"\\n\")\n saveImages(absPath) // stdout 출력 시 입력 파일 기준\n }\n } catch (err) {\n const { sanitizeError } = await import(\"./utils.js\")\n process.stderr.write(`\\n[kordoc] ERROR: ${fileName} — ${sanitizeError(err)}\\n`)\n logger.log({ level: \"error\", stage: \"finalize\", event: \"error\", message: \"CLI 처리 중 예외\", error: { message: sanitizeError(err), name: err instanceof Error ? err.name : \"Error\", stack: err instanceof Error ? err.stack : undefined } })\n process.exitCode = 1\n }\n }\n}\n\n/** 공통 parse 옵션 등록 헬퍼 */\nfunction addParseOptions(cmd: Command): Command {\n return cmd\n .option(\"-o, --output <path>\", \"출력 파일 경로 (단일 파일 시)\")\n .option(\"-d, --out-dir <dir>\", \"출력 디렉토리 (다중 파일 시)\")\n .option(\"-p, --pages <range>\", \"페이지/섹션 범위 (예: 1-3, 1,3,5)\")\n .option(\"--format <type>\", \"출력 형식: markdown (기본) 또는 json\", \"markdown\")\n .option(\"--no-header-footer\", \"PDF 머리글/바닥글 자동 제거\")\n .option(\"--image-dir <dir>\", \"이미지 저장 폴더 (기본: 입력 파일명_images 폴더)\")\n .option(\"--silent\", \"진행 메시지 숨기기\")\n .option(\"--ocr <mode>\", \"OCR 모드: auto(기본), gemini, claude, codex, ollama, off\")\n .option(\"--ocr-jobs <n>\", \"OCR 병렬 처리 수 (기본: 4)\")\n .option(\"--ocr-batch-size <n>\", \"OCR 배치 크기 — CLI당 페이지 수 (기본: gemini/claude 50, codex 100)\")\n}\n\nprogram\n .enablePositionalOptions()\n .name(\"kordoc\")\n .description(\"모두 파싱해버리겠다 — HWP, HWPX, PDF, XLSX, DOCX → Markdown\")\n .version(VERSION)\n\n// `kordoc parse <files>` 서브커맨드 (권장)\naddParseOptions(\n program\n .command(\"parse\")\n .description(\"파일을 마크다운으로 파싱 (HWP, HWPX, PDF, XLSX, DOCX)\")\n .argument(\"<files...>\", \"변환할 파일 경로\")\n).action(runParse)\n\nprogram\n .command(\"convert <input>\")\n .description(\"마크다운 파일을 HWPX 또는 XLSX로 변환\")\n .option(\"-f, --format <type>\", \"출력 포맷: hwpx | xlsx\", \"hwpx\")\n .option(\"-o, --output <path>\", \"출력 파일 경로 (기본: 입력명 + 포맷 확장자)\")\n .option(\"--image-dir <dir>\", \"이미지 폴더 경로 (기본: 입력 MD 파일명_images 폴더)\")\n .option(\"--images\", \"이미지 포함 (기본: 생략, 레이아웃 문제 방지)\")\n .option(\"--template <path>\", \"HWPX 템플릿 파일 경로 (hwpx 전용)\")\n .option(\"--silent\", \"진행 메시지 숨기기\")\n .action(async (input: string, opts) => {\n const validFormats = [\"hwpx\", \"xlsx\"]\n if (!validFormats.includes(opts.format)) {\n process.stderr.write(`[kordoc] 지원하지 않는 포맷: ${opts.format} (hwpx 또는 xlsx)\\n`)\n process.exit(1)\n }\n\n const absInput = resolve(input)\n if (!existsSync(absInput)) {\n process.stderr.write(`[kordoc] 파일을 찾을 수 없습니다: ${input}\\n`)\n process.exit(1)\n }\n\n const stem = basename(absInput).replace(/\\.[^.]+$/, \"\")\n const outPath = opts.output\n ? resolve(opts.output)\n : resolve(absInput, \"..\", `${stem}.${opts.format}`)\n\n if (!opts.silent) process.stderr.write(`[kordoc] ${basename(absInput)} → ${basename(outPath)} ...`)\n\n try {\n const markdown = readFileSync(absInput, \"utf-8\")\n\n // 이미지 폴더에서 이미지 로드 (--images 플래그 필요)\n const imgDir = opts.imageDir ? resolve(opts.imageDir) : resolve(absInput, \"..\", stem + \"_images\")\n const images: import(\"./types.js\").ExtractedImage[] = []\n if (opts.images && existsSync(imgDir)) {\n const mimeMap: Record<string, string> = {\n png: \"image/png\", jpg: \"image/jpeg\", jpeg: \"image/jpeg\",\n gif: \"image/gif\", bmp: \"image/bmp\",\n }\n for (const entry of readdirSync(imgDir, { withFileTypes: true })) {\n if (!entry.isFile()) continue\n const fname = entry.name\n const ext = extname(fname).slice(1).toLowerCase()\n if (!mimeMap[ext]) continue\n const data = readFileSync(resolve(imgDir, fname))\n images.push({ filename: fname, data: new Uint8Array(data), mimeType: mimeMap[ext] })\n }\n if (!opts.silent) process.stderr.write(` → 이미지 ${images.length}개 로드\\n`)\n }\n\n const warnings: string[] = []\n\n let buf: ArrayBuffer\n if (opts.format === \"xlsx\") {\n if (opts.template && !opts.silent) {\n process.stderr.write(`\\n[kordoc] 경고: --template은 hwpx 전용입니다. 무시됩니다.\\n`)\n }\n buf = await markdownToXlsx(markdown, { warnings, images: images.length ? images : undefined })\n } else {\n let templateArrayBuffer: ArrayBuffer | undefined\n if (opts.template) {\n const tmplBuf = readFileSync(resolve(opts.template))\n templateArrayBuffer = tmplBuf.buffer.slice(tmplBuf.byteOffset, tmplBuf.byteOffset + tmplBuf.byteLength)\n }\n buf = await markdownToHwpx(markdown, { warnings, images: images.length ? images : undefined, templateArrayBuffer })\n }\n\n writeFileSync(outPath, Buffer.from(buf))\n\n if (!opts.silent) {\n process.stderr.write(` OK\\n`)\n process.stderr.write(` → ${outPath}\\n`)\n if (warnings.length) warnings.forEach(w => process.stderr.write(` ${w}\\n`))\n }\n } catch (err) {\n const { sanitizeError } = await import(\"./utils.js\")\n process.stderr.write(` FAIL\\n`)\n process.stderr.write(` → ${sanitizeError(err)}\\n`)\n process.exit(1)\n }\n })\n\nprogram\n .command(\"init-env\")\n .description(\"kordoc용 .env 템플릿 생성 (명시 실행 시에만 파일 생성)\")\n .option(\"-o, --output <path>\", \"템플릿 출력 경로\", \".env.kordoc.example\")\n .option(\"--create-dotenv\", \".env 파일이 없으면 함께 생성\")\n .option(\"--force\", \"기존 파일이 있어도 덮어쓰기\")\n .action((opts: { output: string; createDotenv?: boolean; force?: boolean }) => {\n const template = buildEnvTemplate()\n const outPath = resolve(opts.output)\n\n if (existsSync(outPath) && !opts.force) {\n process.stderr.write(`[kordoc] 이미 파일이 있습니다: ${outPath} (덮어쓰려면 --force)\\n`)\n process.exit(1)\n }\n writeFileSync(outPath, template, \"utf-8\")\n process.stderr.write(`[kordoc] 생성됨: ${outPath}\\n`)\n\n if (opts.createDotenv) {\n const dotenvPath = resolve(\".env\")\n if (existsSync(dotenvPath) && !opts.force) {\n process.stderr.write(`[kordoc] .env가 이미 있어 생성하지 않았습니다: ${dotenvPath}\\n`)\n } else {\n writeFileSync(dotenvPath, template, \"utf-8\")\n process.stderr.write(`[kordoc] 생성됨: ${dotenvPath}\\n`)\n }\n }\n })\n\nprogram\n .command(\"watch <dir>\")\n .description(\"디렉토리 감시 — 새 문서 자동 변환\")\n .option(\"--webhook <url>\", \"결과 전송 웹훅 URL\")\n .option(\"-d, --out-dir <dir>\", \"변환 결과 출력 디렉토리\")\n .option(\"-p, --pages <range>\", \"페이지/섹션 범위\")\n .option(\"--format <type>\", \"출력 형식: markdown 또는 json\", \"markdown\")\n .option(\"--silent\", \"진행 메시지 숨기기\")\n .action(async (dir: string, opts) => {\n const { watchDirectory } = await import(\"./watch.js\")\n await watchDirectory({\n dir,\n outDir: opts.outDir,\n webhook: opts.webhook,\n format: opts.format,\n pages: opts.pages,\n silent: opts.silent,\n })\n })\n\n// `kordoc <files>` 루트 커맨드 (하위 호환)\n// 주의: 서브커맨드(init-env 등)보다 뒤에서 등록해야 충돌하지 않음\naddParseOptions(\n program\n .argument(\"<files...>\", \"변환할 파일 경로 (HWP, HWPX, PDF, XLSX, DOCX)\")\n).action(runParse)\n\nprogram.parse()\n"],"mappings":"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","names":[]}
|