Files
hyungi_document_server/evals/markdown/README.md
T
hyungi c1b22d8833 docs(eval): Phase 2 path fix — log_tsv/post-report 는 /app/logs (bind-mount), /app/evals 는 미마운트
cron dry-run 검증 중 발견:
- /app/scripts/ 는 bind-mount 활성 (Phase 2 main FF 후 컨테이너 가시화 ✓)
- /app/evals/ 는 fastapi 이미지에도 없고 compose 마운트도 없음
- 이전 README/plan 의 --log-tsv /app/evals/markdown/... 은 컨테이너
  writable layer 에 쓰여 재기동 시 유실되는 문제

해결: nightly --log-tsv 와 post-report --output-* 는 /app/logs/ 사용
(rw bind-mount → host ~/Documents/code/hyungi_Document_Server/logs/ 영구).
주 1회 git commit 시 logs/ → evals/markdown/ 로 cp 후 add.

post-report 도 동일 패턴.
2026-05-10 05:47:20 +00:00

260 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 1D — Markdown Conversion Pilot 평가
> Plan: `~/.claude/plans/stratified-mingling-otter.md`
> Script: `scripts/phase1d_pilot.py` (subcommands: select / enqueue / report / eval_template)
## 목적
30건 stratified sample 로 marker-pdf 의 **failure mode 종류** 발견. 통계적 대표성이 아니라 **진단 도구**. 결과로 다음 분기점 판정:
- Phase 2 풀 backfill 진입 가능?
- SKIP rule 확장 필요?
- Marker 튜닝 / 대안 (kordoc / OCR 전처리 hybrid) 우선?
## Sample 구성
`pilot_1d_sample.csv` — 30 rows × 15 columns. 시드 `20260502` 고정.
### sample_source 분리
| sample_source | n | 의미 |
|---|---|---|
| `existing_success` | 5 | 기존에 변환 성공한 PDFs. forced anchor (doc 4809 `Note_240805_용접교육 필기`) + calibration 4. **pilot 후 같은 문서 재변환 결과와 비교해 개선 여부 판정 anchor**. |
| `controlled_backfill` | 25 | pending 262건 中 4축 stratified 로 신규 변환. 분포: handwritten 3 / scan_likely 2~3 / mixed 5 / born_digital 12 / large 2 |
### 4 축 stratification
| Axis | Buckets |
|---|---|
| `doc_type` | study_note / Academic_Paper / Reference / Note / Manual / Standard / Specification / NULL |
| `file_size_band` | S (<1MB) / M (1~10MB) / L (>10MB) |
| `text_density_band` | scan-likely (<5 chars/KB) / mixed (5~50) / born-digital (>50) |
| `handwritten_hint` | hi (title/path 매칭: 필기/노트/handwritten/scan/스캔) / lo |
보조 컬럼: `script_mix` (Hangul/CJK/Latin 비율 라벨), `page_count_estimate` (existing_success 만 채워짐), `forced_include_reason`.
## Rubric (사용자 평가, 1~5 점)
각 sample 1건 당 **MarkdownDoc viewer + PDF 원본 토글** 비교하면서 5축 점수 + boolean + notes:
| 축 | 정의 | 1점 | 5점 |
|---|---|---|---|
| **text_accuracy** | OCR/추출 정확도 | 알아보기 어려움, ghost text 다발 | 원본과 거의 동일, OCR 오타 1~2건 |
| **structure** | heading/list/table 구조 보존 | 구조 완전 유실, 한 덩어리 텍스트 | 원본의 heading 계층 + table row 그대로 |
| **noise_rate** | 의미 없는 반복/garbage 토큰 | 본문 30%+ 가 noise | noise 거의 없음 |
| **multi_script** | 한중일/특수문자 혼합 정확도 | 잘못된 스크립트로 mojibake | 원본 스크립트 그대로 보존 |
| **completeness** | 본문 누락 | 페이지 절반 이상 빠짐 | 누락 없음 |
`overall_pass` (true/false) — "이 markdown 으로 검색/참고에 쓸 만한가" 직관 판단. rubric 점수 합계와 별도로 보존.
`notes` — 자유서술. 특히 알려진 failure pattern (예: `TO STAND 12/4` 반복, 한중일 mojibake) 재현 시 명시.
## 평가 워크플로우
### 0. Pre-eval
`evals/markdown/pilot_1d_eval.csv` 가 비어 있다면 (또는 새 라운드면) 스켈레톤 생성:
```bash
ssh hyungi@100.111.160.84 \
"docker compose -f ~/Documents/code/hyungi_Document_Server/docker-compose.yml \
exec fastapi python /app/scripts/phase1d_pilot.py eval_template \
--in /tmp/phase1d_pilot.json \
--csv /app/evals/markdown/pilot_1d_eval.csv"
```
### 1. 한 건씩 평가
브라우저에서 `https://document.hyungi.net/documents/<doc_id>` 열기:
1. 기본 표시 (Markdown 또는 PDF iframe — `canShowMarkdown` 따라) 확인
2. PDF 원본 토글 클릭해서 PDF 와 비교
3. 5축 점수 매기기 (1~5)
4. `overall_pass` true/false 결정
5. notes 에 발견된 failure pattern 기록 (있으면)
6. 결과를 `evals/markdown/pilot_1d_eval.csv` 에 입력
10건씩 3 세션 분할 권장 (총 ~2.5h 사람 시간).
### 2. 의사결정 매트릭스
평가 끝난 30건의 분포로:
| 결과 패턴 | 다음 액션 |
|---|---|
| overall_pass ≥ 25/30 (83%+) 전 영역 | Phase 2 풀 backfill 본 plan 작성. SKIP rule 확장 불필요. |
| overall_pass 20~24 + 특정 영역 (예: 필기) 만 fail | SKIP_DOC_TYPES / source_kind heuristic 으로 약점 영역 제외 → 나머지 풀 backfill |
| overall_pass < 20 또는 systemic 결함 (multi_script 전반 fail 등) | Marker 설정 튜닝 또는 대안 (kordoc vs marker 비교, OCR 전처리 추가) — Phase 1B 재설계 |
| backfill 자체 실패율 > 10% (failed/timeout) | marker-service 안정화 우선. 1D 평가 보류. |
### 3. anchor 비교
`existing_anchor` (doc 4809) 의 평가 결과는 다음 라운드 (Marker 튜닝 또는 대안 도입 후) 같은 문서 재변환 결과와 1:1 비교. 점수 개선 여부가 튜닝 효과의 가장 깨끗한 신호.
### 4. Marker 자가 metrics 와 cross-check
`md_extraction_quality.metrics` (markdown_heading_count / markdown_table_row_count / text_length_ratio 등) 는 Marker 자가 진단. 사람 평가와 비교:
- Marker 가 "tables=237" 인데 사람 평가 structure=1 → 자가 진단 false positive
- text_length_ratio < 1 인데 사람 평가 completeness=5 → ratio 가 좋은 proxy 아닐 수 있음
이런 mismatch 가 `md_extraction_quality.score` 정의의 출발점 (현재 score 항상 null).
## 파일
| 파일 | 역할 | 갱신 시점 |
|---|---|---|
| `pilot_1d_sample.csv` | 30건 sample 정의 (선정 결과). 시드 `20260502` 재현 가능. | select 결과 commit (1회) |
| `pilot_1d_eval.csv` | 사용자 평가 결과 (rubric 점수 + overall_pass + notes) | 사용자 평가 종료 시 commit |
| `README.md` | 본 가이드 | 초기 commit |
## 실행 환경
GPU 서버 fastapi 컨테이너 안에서 실행 — DB / NAS NFS / md_extraction_quality JSONB 접근 필요:
```bash
ssh hyungi@100.111.160.84
cd ~/Documents/code/hyungi_Document_Server
docker compose exec fastapi python /app/scripts/phase1d_pilot.py select \
--csv /app/evals/markdown/pilot_1d_sample.csv
```
**enqueue 의 `--yes` 또는 `--no-dry-run` 류 실행은 별도 사용자 승인 + 야간 단발 sweep 윈도우 (23:00~03:00 KST) 안에서만**. 30건 backfill = marker-service BATCH_SIZE=1 × 평균 5분/건 ≈ 2.5h.
---
# Phase 2 — Full Backfill (legacy pending PDFs)
> Plan: `~/.claude/plans/iridescent-gathering-clover.md`
> Script: `scripts/phase2_backfill.py` (subcommands: inventory / select-canary / enqueue / nightly-enqueue / post-report)
## 목적
1D pilot 결과 = engineering go signal. legacy pending PDF (1D 후 잔여 ~237건) 을 marker_worker 로 변환해 `md_status='success'` 누적. **신규 업로드 우선권 보존, 야간 저부하 sweep, DB state 기반 idempotent checkpoint**.
진행 로드맵: **2-A dry-run inventory → 2-B canary 40건 → 2-C nightly sweep ~4-5 nights → 2-D post-report**.
## 파일
| 파일 | 역할 | 갱신 시점 |
|---|---|---|
| `phase2_inventory.csv` | pending PDFs dry-run inventory + skip forecast | 2-A 종료 (commit, 1회) |
| `phase2_canary_sample.csv` | stratified 40건 canary sample (시드 `20260503`) | 2-B(a) (commit) |
| `phase2_canary_result.md` | canary 결과 요약 + 1D 비교 + GO/HALT 결정 근거 | 2-B 종료 (commit) |
| `phase2_nightly_log.tsv` | 야간 sweep 한 줄/일 (date / enqueued / active_queue_at_start / active_queue_oldest_age_min / pending_pool_remaining / abort_reason / marker_ready) | append 매 sweep, 주 1회 commit |
| `phase2_post_report.csv` | Phase 2 sweep 처리된 doc 별 final state + quality | 2-D (commit) |
| `phase2_post_report.md` | 처리 분포 + 1D baseline 비교 + skip/failed/outlier 목록 | 2-D (commit) |
## Subcommand 사용법
### inventory (read-only, dry-run)
```bash
docker exec hyungi_document_server-fastapi-1 python /app/scripts/phase2_backfill.py inventory \
--output /app/evals/markdown/phase2_inventory.csv
```
- pending PDFs 전체에 대해 doc_id / file_size / text_density / doc_type / forecast_skip_reason 적재.
- forecast_skip_reason ∈ {unsupported_extension / doctype_skip / handwritten_hint / over_max_pages_estimated / none}. 'none' = 변환 시도 후보.
- handwritten_hint = title/path 에 `필기|손글씨|handwritten|handwriting` 매칭 (marker_worker 의 7d0fca2 룰 미러).
- over_max_pages_estimated = file_size > 25MB proxy. 실 page_count 는 marker_worker 가 PyMuPDF 로 결정.
### select-canary (재현성 시드)
```bash
docker exec hyungi_document_server-fastapi-1 python /app/scripts/phase2_backfill.py select-canary \
--inventory /app/evals/markdown/phase2_inventory.csv \
--output /app/evals/markdown/phase2_canary_sample.csv \
--seed 20260503
```
- 40건 buckets: large 6 / scan_likely 2 / study_note 10 / Academic_Paper 8 / Reference 6 / {Standard,Manual,Specification} 4 / {Note,Report,Memo,NULL} 4
- inventory 의 `forecast_skip_reason='none'` 만 선정 후보.
- 시드 고정 → 재실행 시 동일 sample.
### 경로 정책 (2-B canary vs 2-C nightly)
`/app/scripts/``/app/evals/` 는 parent repo (`~/Documents/code/hyungi_Document_Server`, branch=main) 의 read-only bind-mount. `docker cp ... :/app/scripts/...` 는 read-only 위반으로 실패. `docker compose --build` 은 검색 실험 soft lock 위반. → 단계별로 다른 경로 사용:
| 단계 | script 경로 | sample CSV 경로 | 메커니즘 |
|---|---|---|---|
| **2-B canary (pre-merge)** | `/app/logs/phase2_backfill.py` | `/app/logs/phase2_canary_sample.csv` | `docker cp` worktree → /app/logs (rw bind-mount) |
| **2-C nightly (post-merge canonical)** | `/app/scripts/phase2_backfill.py` | `/app/evals/markdown/phase2_*` | feat/phase2-backfill main 머지 + parent `git pull` 후 bind-mount 자동 활성 |
**2-B 임시 sync** (canary 실행 직전 1회):
```bash
docker cp ~/Documents/code/hyungi_Document_Server_phase2/scripts/phase2_backfill.py \
hyungi_document_server-fastapi-1:/app/logs/phase2_backfill.py
docker cp ~/Documents/code/hyungi_Document_Server_phase2/evals/markdown/phase2_canary_sample.csv \
hyungi_document_server-fastapi-1:/app/logs/phase2_canary_sample.csv
```
**2-C 진입 시점** (canary GO 결정 후):
```bash
cd ~/Documents/code/hyungi_Document_Server_phase2 && git push origin feat/phase2-backfill
cd ~/Documents/code/hyungi_Document_Server && git fetch origin \
&& git merge --ff-only origin/feat/phase2-backfill && git push origin main
```
이유: 미검증 코드를 main 에 미리 박지 않음 / canary 결과 따라 worktree 에서 hot-fix 가능 / nightly cron 은 canonical path 사용 (script 자체).
**추가 (2026-05-03)**: nightly cron 의 `--log-tsv` 와 post-report 출력은 `/app/logs/` 사용 (위 표의 canonical path 가 아님). `/app/evals/markdown/` 는 fastapi 컨테이너에 **bind-mount 되어 있지 않아** 컨테이너 writable layer 에 쓰면 컨테이너 재기동 시 유실. `/app/logs/` 는 rw bind-mount → host `~/Documents/code/hyungi_Document_Server/logs/` 에 영구 저장. 주 1회 commit 시 `cp ~/Documents/code/hyungi_Document_Server/logs/phase2_nightly_log.tsv evals/markdown/` 로 복사 후 git add.
### enqueue (one-shot, 사용자 승인 게이트)
```bash
# dry-run (default) — 2-B 단계 = /app/logs 임시 경로 (위 §"경로 정책" 참조)
docker exec hyungi_document_server-fastapi-1 python /app/logs/phase2_backfill.py enqueue \
--csv /app/logs/phase2_canary_sample.csv
# actual (사용자 승인 후)
docker exec hyungi_document_server-fastapi-1 python /app/logs/phase2_backfill.py enqueue \
--csv /app/logs/phase2_canary_sample.csv --no-dry-run
```
- marker-service `/ready` 사전 검증.
- `enqueue_stage` idempotent — 중복 호출 안전.
### nightly-enqueue (cron / manual)
```bash
docker exec hyungi_document_server-fastapi-1 python /app/scripts/phase2_backfill.py nightly-enqueue \
--limit 50 --max-active-queue 5 \
--log-tsv /app/logs/phase2_nightly_log.tsv # /app/evals/ 미 bind-mount, /app/logs/ rw 사용
```
- 가드 순서: disable flag (`/tmp/phase2_disable`) → marker /ready → active_queue ≤ threshold → DB pool 비어있지 않음 → enqueue.
- 매 sweep log_tsv 한 줄. abort_reason ∈ {disable_flag / marker_unhealthy / active_queue_threshold / pool_empty / empty}.
- pool_empty = Phase 2 자연 완료 신호 (cron 제거 hard gate trigger).
### post-report
```bash
docker exec hyungi_document_server-fastapi-1 python /app/scripts/phase2_backfill.py post-report \
--output-csv /app/logs/phase2_post_report.csv \
--output-md /app/logs/phase2_post_report.md \
--phase2-start 2026-05-03T00:00:00Z
```
- `--phase2-start` ISO timestamp 이후 `md_generated_at` 만 집계 (Phase 2 코드 push 시점 권장).
- 1D baseline (success 92% / elapsed_p50 34s / text_length_ratio_p50 1.15) 와 비교.
- outlier 후보: elapsed_ms > 300s, text_length_ratio < 0.5 또는 > 10, 신규 warning 종류.
## 의사결정 게이트
### 2-B canary GO/HALT
- success ≥ 36/40 (90%) AND failed ≤ 2 AND skipped ≤ 6 → **2-C 진입 GO**
- 위 미충족 → HALT, 사용자 보고 후 재검토
### 2-C nightly abort
- 1 night 안에 failed > 5 → script 가 disable flag 자동 생성
- marker-service `/ready` 실패 → 그 sweep 건너뜀, 다음 sweep 재시도
- active_queue_oldest_age_min > 60 (stuck 임계) → log 에 [warn], 사용자 morning check 으로 판단
### 2-D 종료 hard gate (Phase 2 closed 선언 직전)
- (cron 모드) `crontab -l | grep phase2_backfill` 결과 비어 있어야 함
- `~/.phase2_disable` 파일 정리 됨
- pending PDF (`md_status='pending'`, `file_format='pdf'`) ≤ 5
- processing_queue markdown active = 0
## 1D 와의 차이
| 항목 | 1D | Phase 2 |
|---|---|---|
| 목적 | failure mode 진단 | 풀 변환 |
| 대상 | 30건 stratified | 237건 잔여 |
| sample_source | existing_success + controlled_backfill | controlled_backfill only |
| 처리 모드 | one-shot cron (1회) | nightly cron (~4-5 nights) |
| 평가 | 사용자 5축 rubric | marker 자가 metrics + 1D baseline 비교 |
| anchor 보존 | doc 4809 forced_include | (재처리 안 함) |
| handwritten | over-sample (3건) | marker_worker 자동 skip 신뢰 |