openfree commited on
Commit
1ae2310
·
verified ·
1 Parent(s): f5141ed

feat: HWP/HWPX 실제 추출 (rhwp WASM + Node 22 Docker) — getSectionCount/getParagraphCount fix

Browse files
Files changed (5) hide show
  1. Dockerfile +9 -3
  2. app.py +39 -9
  3. pkg/extract_hwp.mjs +79 -0
  4. pkg/rhwp.js +0 -0
  5. pkg/rhwp_bg.wasm +3 -0
Dockerfile CHANGED
@@ -1,14 +1,20 @@
1
  FROM python:3.12-slim
2
- WORKDIR /app
3
 
4
- # system deps
5
  RUN apt-get update && apt-get install -y --no-install-recommends \
6
- curl ca-certificates \
 
 
7
  && rm -rf /var/lib/apt/lists/*
8
 
 
 
9
  COPY requirements.txt .
10
  RUN pip install --no-cache-dir -r requirements.txt
11
 
 
 
 
12
  COPY app.py .
13
  COPY static/ ./static/
14
 
 
1
  FROM python:3.12-slim
 
2
 
3
+ # Node.js 22 (rhwp.js WASM HWP/HWPX 추출용 — HanSoo 이식)
4
  RUN apt-get update && apt-get install -y --no-install-recommends \
5
+ curl ca-certificates gnupg \
6
+ && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
7
+ && apt-get install -y --no-install-recommends nodejs \
8
  && rm -rf /var/lib/apt/lists/*
9
 
10
+ WORKDIR /app
11
+
12
  COPY requirements.txt .
13
  RUN pip install --no-cache-dir -r requirements.txt
14
 
15
+ # HWP 추출 (rhwp WASM via Node)
16
+ COPY pkg/ ./pkg/
17
+
18
  COPY app.py .
19
  COPY static/ ./static/
20
 
app.py CHANGED
@@ -185,18 +185,48 @@ def extract_txt(data: bytes) -> str:
185
  except UnicodeDecodeError: continue
186
  return data.decode("utf-8", errors="replace")
187
 
 
 
 
 
188
  async def extract_hwp(data: bytes, fname: str) -> str:
189
- if not HWP_API_URL:
190
- return f"[HWP/HWPX 파일 {fname} — 서버에 HanSoo 변환 API 설정 안 됨 (env JGOS_HWP_API_URL). 파일 내용 추출 불가.]"
 
 
 
 
 
191
  try:
192
- async with httpx.AsyncClient(timeout=60) as client:
193
- r = await client.post(HWP_API_URL, files={"file": (fname, data)})
194
- if r.status_code == 200:
195
- j = r.json()
196
- return j.get("text", f"[HanSoo API 응답 text 없음]")
197
- return f"[HanSoo API HTTP {r.status_code}]"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  except Exception as e:
199
- return f"[HanSoo API 오류: {type(e).__name__}: {e}]"
 
 
 
 
200
 
201
  def extract_image_meta(data: bytes, fname: str) -> str:
202
  """이미지: 메타만 텍스트로. 실제 vision 분석은 base64로 LLM에 직접 전달."""
 
185
  except UnicodeDecodeError: continue
186
  return data.decode("utf-8", errors="replace")
187
 
188
+ import tempfile, asyncio as _asyncio
189
+ PKG_DIR = Path(__file__).parent / "pkg"
190
+ RHWP_SCRIPT = PKG_DIR / "extract_hwp.mjs"
191
+
192
  async def extract_hwp(data: bytes, fname: str) -> str:
193
+ """HanSoo rhwp WASM (Rust) — Node.js subprocess로 HWP/HWPX 한글 텍스트 직접 추출.
194
+ 외부 API 의존성 0. JGOS Space 내부 처리.
195
+ """
196
+ if not RHWP_SCRIPT.exists():
197
+ return f"[HWP 처리기 미설치: {RHWP_SCRIPT}]"
198
+ suffix = ".hwpx" if fname.lower().endswith(".hwpx") else ".hwp"
199
+ tmp_path = None
200
  try:
201
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tf:
202
+ tf.write(data); tf.flush(); tmp_path = tf.name
203
+ proc = await _asyncio.create_subprocess_exec(
204
+ "node", str(RHWP_SCRIPT), tmp_path,
205
+ stdout=_asyncio.subprocess.PIPE, stderr=_asyncio.subprocess.PIPE,
206
+ )
207
+ try:
208
+ stdout, stderr = await _asyncio.wait_for(proc.communicate(), timeout=60)
209
+ except _asyncio.TimeoutError:
210
+ proc.kill(); return f"[HWP 변환 타임아웃 60s: {fname}]"
211
+ if proc.returncode != 0:
212
+ err = (stderr.decode("utf-8", errors="ignore")[:300]) if stderr else "no stderr"
213
+ return f"[HWP 변환 실패 (exit {proc.returncode}): {err}]"
214
+ try:
215
+ result = json.loads(stdout.decode("utf-8", errors="ignore"))
216
+ except Exception as e:
217
+ return f"[HWP 결과 파싱 실패: {e}]"
218
+ if not result.get("ok"):
219
+ return f"[HWP 추출 실패: {result.get('error','unknown')}]"
220
+ text = result.get("text", "")
221
+ return text or f"[{fname}: 추출 텍스트 없음 (paragraphs={result.get('paragraphs',0)})]"
222
+ except FileNotFoundError as e:
223
+ return f"[Node.js 미설치 — Docker rebuild 필요: {e}]"
224
  except Exception as e:
225
+ return f"[HWP 처리 오류: {type(e).__name__}: {e}]"
226
+ finally:
227
+ if tmp_path:
228
+ try: os.unlink(tmp_path)
229
+ except: pass
230
 
231
  def extract_image_meta(data: bytes, fname: str) -> str:
232
  """이미지: 메타만 텍스트로. 실제 vision 분석은 base64로 LLM에 직접 전달."""
pkg/extract_hwp.mjs ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // extract_hwp.mjs — HWP/HWPX 파일을 텍스트로 추출 (rhwp WASM 사용)
2
+ // 사용: node extract_hwp.mjs <input_hwp_path>
3
+ // 출력: stdout JSON {ok, text, paragraphs, sections, error?}
4
+
5
+ import init, { HwpDocument } from "./rhwp.js";
6
+ import { readFile } from "node:fs/promises";
7
+
8
+ async function main() {
9
+ const inputPath = process.argv[2];
10
+ if (!inputPath) {
11
+ console.error(JSON.stringify({ ok: false, error: "missing arg: input path" }));
12
+ process.exit(1);
13
+ }
14
+
15
+ try {
16
+ // WASM 초기화 — Node.js fs로 직접 로드
17
+ const { readFileSync } = await import("node:fs");
18
+ const wasmPath = new URL("./rhwp_bg.wasm", import.meta.url);
19
+ const wasmBytes = readFileSync(wasmPath);
20
+ await init({ module_or_path: wasmBytes });
21
+
22
+ // HWP 파일 로드
23
+ const fileBytes = await readFile(inputPath);
24
+ const doc = new HwpDocument(new Uint8Array(fileBytes));
25
+
26
+ // Method 1: getTextRange로 모든 section/paragraph 순회
27
+ const allParas = [];
28
+ let sectionCount = 0;
29
+ try {
30
+ sectionCount = doc.getSectionCount();
31
+ } catch (e) {
32
+ sectionCount = 1;
33
+ }
34
+
35
+ for (let s = 0; s < sectionCount; s++) {
36
+ let paraCount = 0;
37
+ try {
38
+ paraCount = doc.getParagraphCount(s);
39
+ } catch (e) { continue; }
40
+
41
+ for (let p = 0; p < paraCount; p++) {
42
+ let paraLen = 0;
43
+ try {
44
+ paraLen = doc.getParagraphLength(s, p);
45
+ } catch (e) { continue; }
46
+ if (paraLen === 0) { allParas.push(""); continue; }
47
+ try {
48
+ const text = doc.getTextRange(s, p, 0, paraLen);
49
+ allParas.push(text || "");
50
+ } catch (e) {
51
+ allParas.push(`[para ${s}.${p} 추출 실패]`);
52
+ }
53
+ }
54
+ }
55
+
56
+ const finalText = allParas.filter(p => p && p.trim()).join("\n");
57
+
58
+ // free
59
+ try { doc.free(); } catch (e) {}
60
+
61
+ console.log(JSON.stringify({
62
+ ok: true,
63
+ text: finalText,
64
+ paragraphs: allParas.length,
65
+ sections: sectionCount,
66
+ char_count: finalText.length,
67
+ }, null, 0));
68
+
69
+ } catch (e) {
70
+ console.error(JSON.stringify({
71
+ ok: false,
72
+ error: `${e.name || "Error"}: ${e.message || e}`,
73
+ stack: (e.stack || "").split("\n").slice(0, 5).join(" | "),
74
+ }));
75
+ process.exit(2);
76
+ }
77
+ }
78
+
79
+ main();
pkg/rhwp.js ADDED
The diff for this file is too large to render. See raw diff
 
pkg/rhwp_bg.wasm ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:72dd73f7343948584bc7f8b0ffcd7c8f5d05489a63b41b1f657ea41b24095bea
3
+ size 3703777