Skip to content

Commit 4ae2c46

Browse files
serithemageclaude
andcommitted
feat: Add Upstage Document Parse as Stage 1 parser option
- Add internal/parser/upstage package with Document Parse API client - Support --parser flag (native, upstage) and HWP2MD_PARSER env - Add RawMarkdown field to IR for direct markdown passthrough - Request markdown output format from Upstage API - Add Stage 1 parser comparison in README Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 497bee0 commit 4ae2c46

7 files changed

Lines changed: 2004 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
- Upstage Document Parse as alternative Stage 1 parser (`--parser upstage`, `HWP2MD_PARSER=upstage`)
12+
- Direct HWP/HWPX file support via Upstage API
13+
- OCR-based layout recognition for complex documents
14+
1015
## [0.2.0] - 2025-01-10
1116

1217
### Added

README.md

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ flowchart LR
2020
end
2121
2222
subgraph Stage1[Stage 1: Parser]
23-
HWPX[HWPX Parser]
24-
IR[IR - 중간 표현]
25-
HWPX --> IR
23+
direction TB
24+
Parser{Parser}
25+
Parser --> |native| HWPX[HWPX Parser]
26+
Parser --> |upstage| Upstage[Upstage Document Parse]
27+
HWPX --> IR[IR - 중간 표현]
28+
Upstage --> IR
2629
end
2730
2831
subgraph Stage2[Stage 2: LLM - 선택적]
@@ -39,7 +42,7 @@ flowchart LR
3942
MD2[향상된 Markdown]
4043
end
4144
42-
HWP --> HWPX
45+
HWP --> Parser
4346
IR --> MD1
4447
IR -.-> LLM
4548
LLM -.-> MD2
@@ -86,6 +89,22 @@ hwp2markdown document.hwpx
8689
> **Note**: `convert` 명령어는 기본 명령이므로 생략할 수 있습니다.
8790
> `hwp2markdown document.hwpx``hwp2markdown convert document.hwpx`는 동일합니다.
8891
92+
### Upstage Document Parse 사용 (Stage 1)
93+
94+
내장 파서 대신 [Upstage Document Parse API](https://www.upstage.ai/document-parse)를 사용하여 문서를 파싱할 수 있습니다.
95+
96+
```bash
97+
# Upstage Document Parse 사용
98+
export UPSTAGE_API_KEY="your-api-key"
99+
hwp2markdown document.hwpx --parser upstage
100+
101+
# 환경변수로 설정
102+
export HWP2MD_PARSER="upstage"
103+
hwp2markdown document.hwpx
104+
```
105+
106+
Upstage Document Parse는 HWP/HWPX 파일을 직접 지원하며, OCR 기반으로 복잡한 레이아웃을 더 정확하게 인식할 수 있습니다.
107+
89108
### LLM 포맷팅 (Stage 2)
90109

91110
LLM을 사용하면 더 자연스럽고 읽기 쉬운 Markdown을 생성할 수 있습니다.
@@ -125,13 +144,14 @@ hwp2markdown extract document.hwpx --format text
125144

126145
| 변수 | 설명 |
127146
|------|------|
147+
| `HWP2MD_PARSER` | 파서 선택 (`native`, `upstage`) - 기본: native |
128148
| `HWP2MD_LLM` | `true`로 설정하면 LLM 포맷팅 활성화 |
129149
| `HWP2MD_MODEL` | 사용할 모델 이름 (프로바이더 자동 감지) |
130150
| `HWP2MD_BASE_URL` | 프라이빗 API 엔드포인트 (Bedrock, Azure, 로컬 서버) |
131151
| `ANTHROPIC_API_KEY` | Anthropic API 키 |
132152
| `OPENAI_API_KEY` | OpenAI API 키 |
133153
| `GOOGLE_API_KEY` | Google Gemini API 키 |
134-
| `UPSTAGE_API_KEY` | Upstage API 키 |
154+
| `UPSTAGE_API_KEY` | Upstage API 키 (LLM 및 Document Parse) |
135155
| `OLLAMA_HOST` | Ollama 서버 주소 (기본: http://localhost:11434) |
136156

137157
모델 이름으로 프로바이더가 자동 감지됩니다:
@@ -216,7 +236,8 @@ hwp2markdown/
216236
│ │ ├── upstage/ # Upstage Solar
217237
│ │ └── ollama/ # Local Ollama
218238
│ └── parser/ # 문서 파서
219-
│ └── hwpx/ # HWPX 파서
239+
│ ├── hwpx/ # HWPX 파서 (내장)
240+
│ └── upstage/ # Upstage Document Parse
220241
├── docs/ # 문서
221242
└── tests/ # 테스트 데이터
222243
```
@@ -225,12 +246,15 @@ hwp2markdown/
225246

226247
실제 변환 결과를 확인하여 품질을 평가할 수 있습니다.
227248

228-
### Stage 1 (Parser)
249+
### Stage 1 (Parser 비교)
229250

230-
| 파일 | 설명 |
231-
|------|------|
232-
| [원본 HWPX](testdata/한글%20테스트.hwpx) | 테스트용 한글 문서 (공무원 채용 공고) |
233-
| [Stage 1 결과](testdata/한글%20테스트_stage1.md) | 파서만 사용한 기본 Markdown 변환 |
251+
동일한 문서를 내장 파서와 Upstage Document Parse로 변환한 결과를 비교할 수 있습니다.
252+
253+
| 파서 | 결과 | 설명 |
254+
|------|------|------|
255+
| 원본 | [한글 테스트.hwpx](testdata/한글%20테스트.hwpx) | 테스트용 한글 문서 (공무원 채용 공고) |
256+
| Native | [결과 보기](testdata/한글%20테스트_stage1.md) | 내장 HWPX 파서 |
257+
| Upstage | [결과 보기](testdata/한글%20테스트_stage1_upstage.md) | Upstage Document Parse API |
234258

235259
### Stage 2 (LLM 비교)
236260

internal/cli/convert.go

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import (
1414
"github.com/roboco-io/hwp2markdown/internal/llm/gemini"
1515
"github.com/roboco-io/hwp2markdown/internal/llm/ollama"
1616
"github.com/roboco-io/hwp2markdown/internal/llm/openai"
17-
"github.com/roboco-io/hwp2markdown/internal/llm/upstage"
17+
llmupstage "github.com/roboco-io/hwp2markdown/internal/llm/upstage"
1818
"github.com/roboco-io/hwp2markdown/internal/parser"
1919
"github.com/roboco-io/hwp2markdown/internal/parser/hwpx"
20+
parserupstage "github.com/roboco-io/hwp2markdown/internal/parser/upstage"
2021
"github.com/spf13/cobra"
2122
)
2223

@@ -26,6 +27,7 @@ var (
2627
convertProvider string
2728
convertModel string
2829
convertBaseURL string
30+
convertParser string
2931
convertExtractImgs bool
3032
convertImagesDir string
3133
convertVerbose bool
@@ -42,6 +44,7 @@ var convertCmd = &cobra.Command{
4244
더 자연스러운 Markdown을 생성할 수 있습니다.
4345
4446
환경 변수:
47+
HWP2MD_PARSER=xxx 파서 선택 (native, upstage)
4548
HWP2MD_LLM=true Stage 2 활성화
4649
HWP2MD_MODEL=xxx 모델 이름 (프로바이더 자동 감지)
4750
HWP2MD_BASE_URL=xxx 프라이빗 API 엔드포인트 (Bedrock, 로컬 서버 등)
@@ -58,9 +61,14 @@ var convertCmd = &cobra.Command{
5861
--base-url http://localhost:8080 # 로컬 서버
5962
--base-url https://your-azure-endpoint.openai.azure.com # Azure OpenAI
6063
64+
파서 선택:
65+
--parser=native 내장 파서 사용 (기본)
66+
--parser=upstage Upstage Document Parse API 사용 (UPSTAGE_API_KEY 필요)
67+
6168
예시:
6269
hwp2markdown convert document.hwpx
6370
hwp2markdown convert document.hwpx -o output.md
71+
hwp2markdown convert document.hwpx --parser upstage
6472
hwp2markdown convert document.hwpx --llm
6573
hwp2markdown convert document.hwpx --llm --model gpt-4o
6674
hwp2markdown convert document.hwpx --llm --model solar-pro
@@ -76,6 +84,7 @@ func init() {
7684
convertCmd.Flags().StringVar(&convertProvider, "provider", "", "LLM 프로바이더 (openai, anthropic, gemini, upstage, ollama)")
7785
convertCmd.Flags().StringVar(&convertModel, "model", "", "LLM 모델 이름")
7886
convertCmd.Flags().StringVar(&convertBaseURL, "base-url", "", "프라이빗 API 엔드포인트 (Bedrock, Azure, 로컬 서버 등)")
87+
convertCmd.Flags().StringVar(&convertParser, "parser", "", "파서 선택 (native, upstage)")
7988
convertCmd.Flags().BoolVar(&convertExtractImgs, "extract-images", false, "이미지 추출 활성화")
8089
convertCmd.Flags().StringVar(&convertImagesDir, "images-dir", "./images", "추출된 이미지 저장 디렉토리")
8190
convertCmd.Flags().BoolVarP(&convertVerbose, "verbose", "v", false, "상세 출력")
@@ -103,8 +112,21 @@ func runConvert(cmd *cobra.Command, args []string) error {
103112
fmt.Fprintf(cmd.ErrOrStderr(), "파일 형식: %s\n", format)
104113
}
105114

115+
// Determine parser type (from flag or env)
116+
parserType := convertParser
117+
if parserType == "" {
118+
parserType = os.Getenv("HWP2MD_PARSER")
119+
}
120+
if parserType == "" {
121+
parserType = "native"
122+
}
123+
124+
if !convertQuiet && convertVerbose {
125+
fmt.Fprintf(cmd.ErrOrStderr(), "파서: %s\n", parserType)
126+
}
127+
106128
// Parse document (Stage 1)
107-
doc, err := parseDocumentForConvert(inputPath, format)
129+
doc, err := parseDocumentForConvert(cmd, inputPath, format, parserType)
108130
if err != nil {
109131
return fmt.Errorf("문서 파싱 실패: %w", err)
110132
}
@@ -153,7 +175,23 @@ func runConvert(cmd *cobra.Command, args []string) error {
153175
return nil
154176
}
155177

156-
func parseDocumentForConvert(path string, format parser.Format) (*ir.Document, error) {
178+
func parseDocumentForConvert(cmd *cobra.Command, path string, format parser.Format, parserType string) (*ir.Document, error) {
179+
// Use Upstage Document Parse API if selected
180+
if parserType == "upstage" {
181+
upstageParser, err := parserupstage.New(parserupstage.Config{})
182+
if err != nil {
183+
return nil, fmt.Errorf("Upstage 파서 초기화 실패: %w", err)
184+
}
185+
186+
if !convertQuiet {
187+
fmt.Fprintf(cmd.ErrOrStderr(), "Upstage Document Parse API 사용 중...\n")
188+
}
189+
190+
ctx := context.Background()
191+
return upstageParser.Parse(ctx, path)
192+
}
193+
194+
// Native parser
157195
opts := parser.Options{
158196
ExtractImages: convertExtractImgs,
159197
ImageDir: convertImagesDir,
@@ -169,7 +207,8 @@ func parseDocumentForConvert(path string, format parser.Format) (*ir.Document, e
169207
return p.Parse()
170208

171209
case parser.FormatHWP:
172-
return nil, fmt.Errorf("HWP 5.x 형식은 아직 지원하지 않습니다")
210+
// Native parser doesn't support HWP, suggest using Upstage
211+
return nil, fmt.Errorf("HWP 5.x 형식은 내장 파서에서 지원하지 않습니다. --parser=upstage 옵션을 사용하세요")
173212

174213
default:
175214
return nil, fmt.Errorf("알 수 없는 형식: %s", format)
@@ -239,7 +278,7 @@ func formatWithLLM(cmd *cobra.Command, doc *ir.Document) (string, *llm.FormatRes
239278
Model: model,
240279
})
241280
case "upstage":
242-
provider, err = upstage.New(upstage.Config{
281+
provider, err = llmupstage.New(llmupstage.Config{
243282
Model: model,
244283
BaseURL: baseURL,
245284
})
@@ -270,6 +309,24 @@ func formatWithLLM(cmd *cobra.Command, doc *ir.Document) (string, *llm.FormatRes
270309
}
271310

272311
func convertToBasicMarkdown(doc *ir.Document) string {
312+
// If RawMarkdown is available (e.g., from Upstage parser), use it directly
313+
if doc.RawMarkdown != "" {
314+
var sb strings.Builder
315+
// Add front matter if metadata exists
316+
if doc.Metadata.Title != "" || doc.Metadata.Author != "" {
317+
sb.WriteString("---\n")
318+
if doc.Metadata.Title != "" {
319+
sb.WriteString(fmt.Sprintf("title: %s\n", doc.Metadata.Title))
320+
}
321+
if doc.Metadata.Author != "" {
322+
sb.WriteString(fmt.Sprintf("author: %s\n", doc.Metadata.Author))
323+
}
324+
sb.WriteString("---\n\n")
325+
}
326+
sb.WriteString(doc.RawMarkdown)
327+
return sb.String()
328+
}
329+
273330
var sb strings.Builder
274331

275332
// Metadata as YAML front matter (optional)

internal/ir/ir.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ package ir
44

55
// Document represents the intermediate representation of an HWP document.
66
type Document struct {
7-
Version string `json:"version"`
8-
Metadata Metadata `json:"metadata"`
9-
Content []Block `json:"content"`
7+
Version string `json:"version"`
8+
Metadata Metadata `json:"metadata"`
9+
Content []Block `json:"content"`
10+
RawMarkdown string `json:"raw_markdown,omitempty"` // Pre-rendered markdown from external parser (e.g., Upstage)
1011
}
1112

1213
// Metadata contains document metadata.

0 commit comments

Comments
 (0)