From b09687d41d2761e707b2452a51fcf3e571f8fcbd Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Sat, 2 May 2026 16:15:09 +0900 Subject: [PATCH] =?UTF-8?q?feat(scripts):=20Phase=201D=20Round=202=20?= =?UTF-8?q?=E2=80=94=20controlled=20backfill=20stratification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 phase1d_pilot.py (단순 ai_domain × file_size 3-bucket) 를 plan ~/.claude/plans/stratified-mingling-otter.md 의 4축 + sample_source 분리 + forced_include 로 augment. Round 1 (ai_domain × file_size 3-bucket) 의 한계: pending PDFs 의 자연 분포만 반영 → 알려진 약점 (필기/스캔/한중일 mixed OCR) 이 sample 에 안 들어옴. 1C 시각 확인에서 doc 4809 (Note_240805_용접교육 필기) 가 실제로 그 패턴을 보였는데, 자연 selection 에 맡기면 다음 라운드도 같은 case 가 빠질 위험. Round 2 디자인: - 4 축 stratification: doc_type × file_size_band × text_density_band × handwritten_hint - sample_source ∈ {existing_success(5), controlled_backfill(25)} - forced_include doc 4809 — known bad anchor. 다음 튜닝/대안 도입 후 같은 문서 재변환 결과와 1:1 비교 가능. - text_density = LENGTH(extracted_text) / (file_size / 1024) chars/KB 가장 깨끗한 단일 proxy. 0.17(필기 4809) ↔ 94(born-digital 3759) 양 끝 검증. - script_mix proxy: Hangul/CJK/Hiragana/Katakana/Latin Unicode block ratio → korean_dominant / mixed_korean_cjk / mixed_korean_latin / cjk_dominant / latin_dominant / unknown. - page_count_estimate: existing_success 는 md_extraction_quality. metrics.source_page_count 사용. controlled_backfill 은 NULL (marker 가 PyMuPDF 로 어차피 다시 읽음). - 시드 SAMPLE_SEED=20260502 고정, 재현성 보장. Sample 분포 (실측 2026-05-02): bucket_label: born_digital=12, mixed=5, existing_calibration=4, handwritten=3, scan_likely=3, large=2, existing_anchor=1 doc_type: Academic_Paper=7, study_note=6, Standard=5, Note=4, Reference=3, Manual=3, Drawing=1, Report=1 file_size_band: M=14, S=12, L=4 text_density_band: born-digital=15, scan-likely=9, mixed=6 handwritten_hint: lo=26, hi=4 (모집단 1.1% 대비 13배 over-sample) forced anchor doc 4809 = density 0.17 (사용자 시각 확인의 그 문서) 새 subcommand: eval_template — pilot_1d_eval.csv 스켈레톤 (rubric 5축 1~5 + overall_pass + notes). 사용자가 MarkdownDoc + PDF 토글 비교하며 점수 채움. 기존 cmd_enqueue (snapshot/backup/dedup) + cmd_report (quality 메트릭) 는 유지. 산출물: scripts/phase1d_pilot.py — 4축 + sample_source + forced_include + eval_template subcommand. CSV+JSON dual output. evals/markdown/README.md — rubric + decision matrix + workflow guide. evals/markdown/pilot_1d_sample.csv — 30 rows × 15 cols (시드 결과, 재현성 보존). evals/markdown/pilot_1d_eval.csv — 빈 스켈레톤 (사용자 평가 후 채움). 실행 경계: Step 1~3 (selection / template / dry-run) = 본 PR 으로 완료. Step 4 (--yes enqueue, 실제 30건 markdown 큐 인입) = 사용자 timing 승인 + 야간 단발 sweep 윈도우 (23:00~03:00 KST) 안에서 별도 실행. marker-service BATCH_SIZE=1, 30건 평균 5분/건 ≈ 2.5h. Verify: GPU 서버 fastapi 컨테이너에서 select 실행 → 30건 sample CSV 생성됨. eval_template subcommand 동작 확인. enqueue dry-run 으로 30 doc_ids + snapshot 출력 후 사용자 취소 분기 확인. Co-Authored-By: Claude Opus 4.7 (1M context) --- evals/markdown/README.md | 119 +++++++ evals/markdown/pilot_1d_eval.csv | 31 ++ evals/markdown/pilot_1d_sample.csv | 31 ++ scripts/phase1d_pilot.py | 514 ++++++++++++++++++++++++----- 4 files changed, 616 insertions(+), 79 deletions(-) create mode 100644 evals/markdown/README.md create mode 100644 evals/markdown/pilot_1d_eval.csv create mode 100644 evals/markdown/pilot_1d_sample.csv diff --git a/evals/markdown/README.md b/evals/markdown/README.md new file mode 100644 index 0000000..a9ef9c2 --- /dev/null +++ b/evals/markdown/README.md @@ -0,0 +1,119 @@ +# 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/` 열기: +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. diff --git a/evals/markdown/pilot_1d_eval.csv b/evals/markdown/pilot_1d_eval.csv new file mode 100644 index 0000000..f3eaacd --- /dev/null +++ b/evals/markdown/pilot_1d_eval.csv @@ -0,0 +1,31 @@ +doc_id,title,sample_source,bucket_label,text_accuracy,structure,noise_rate,multi_script,completeness,overall_pass,notes +4809,Note_240805_용접교육 필기,existing_success,existing_anchor,,,,,,, +5248,작업자 재난안전사고 예방을 위한 위험성평가 기법 연구,existing_success,existing_calibration,,,,,,, +4068,공업역학 동역학(제13판)_Chapter 21 3차원 강체 운동역학,existing_success,existing_calibration,,,,,,, +5189,VIII-1_08-UB,existing_success,existing_calibration,,,,,,, +5141,Structural Analysiss and Design of Process Equipment_00_Contents,existing_success,existing_calibration,,,,,,, +4815,Note_240830_소음진동교육 필기,controlled_backfill,handwritten,,,,,,, +4798,Note_240528_다이아프람워크숍,controlled_backfill,handwritten,,,,,,, +4813,Note_240827_필기,controlled_backfill,handwritten,,,,,,, +5151,THE PIPE FABRICATORS BLUE BOOK,controlled_backfill,scan_likely,,,,,,, +5268,황현필의 진보를 위한 역사_6장 제주4-3사건의 왜국을 멈추라,controlled_backfill,scan_likely,,,,,,, +5127,표준기계설계(KS)_08_핀,controlled_backfill,scan_likely,,,,,,, +8855,2월 26일,controlled_backfill,mixed,,,,,,, +4061,공업역학 동역학(제13판)_Chapter 14 질점의 운동역학_일과 에너지,controlled_backfill,mixed,,,,,,, +3782,"Safety and Health for Engineers_02_5 Local, International, and Voluntary Laws, Regulations, and Standards",controlled_backfill,mixed,,,,,,, +5179,Hydrogen-Embrittlement,controlled_backfill,mixed,,,,,,, +5133,압력용기 핸드북_기타,controlled_backfill,mixed,,,,,,, +3757,Industrial Safety and Health Management(7-ED)_2 Development of the safety and Health Function,controlled_backfill,born_digital,,,,,,, +3758,Industrial Safety and Health Management(7-ED)_3 Concepts of Hazard Avoidance,controlled_backfill,born_digital,,,,,,, +5163,국내 지속가능경영보고서의 노동인권 분야에 대한 실태 분석,controlled_backfill,born_digital,,,,,,, +5167,우리나라 기업의 환경정보 공시 현황과 제도적 개선방안,controlled_backfill,born_digital,,,,,,, +5154,국내 금속가공 중소기업의 스마트팩토리 활용 정도에 대한 실증적 연구,controlled_backfill,born_digital,,,,,,, +5155,스마트 팩토리의 전략적 활용 연구,controlled_backfill,born_digital,,,,,,, +5137,Pressure Vessel Design Manual_01 General Topics,controlled_backfill,born_digital,,,,,,, +5211,PTB-4-2013_00_Foreword,controlled_backfill,born_digital,,,,,,, +5178,Hydrogen_Piping_and_Pipelines_ASME_Code,controlled_backfill,born_digital,,,,,,, +5168,TCoYourPaperlessOffice-4.0,controlled_backfill,born_digital,,,,,,, +3765,Industrial Safety and Health Management(7-ED)_10 Environmental Control and Noise,controlled_backfill,born_digital,,,,,,, +3769,Industrial Safety and Health Management(7-ED)_14 Materials Handling and Storage,controlled_backfill,born_digital,,,,,,, +5274,황현필의 진보를 위한 역사_12장 대한민국의 정신을 훼손하지 말라,controlled_backfill,large,,,,,,, +5180,ASME Sec I 2025,controlled_backfill,large,,,,,,, diff --git a/evals/markdown/pilot_1d_sample.csv b/evals/markdown/pilot_1d_sample.csv new file mode 100644 index 0000000..26e02ea --- /dev/null +++ b/evals/markdown/pilot_1d_sample.csv @@ -0,0 +1,31 @@ +doc_id,title,sample_source,forced_include_reason,bucket_label,doc_type,file_size,file_size_band,text_len,text_density,text_density_band,handwritten_hint,scan_likely,script_mix,page_count_estimate +4809,Note_240805_용접교육 필기,existing_success,known_bad_handwritten_anchor,existing_anchor,Note,1089182,M,177,0.166,scan-likely,hi,true,latin_dominant, +5248,작업자 재난안전사고 예방을 위한 위험성평가 기법 연구,existing_success,,existing_calibration,Academic_Paper,942262,S,3191,3.468,scan-likely,lo,true,unknown, +4068,공업역학 동역학(제13판)_Chapter 21 3차원 강체 운동역학,existing_success,,existing_calibration,Academic_Paper,5429661,M,45253,8.534,mixed,lo,false,mixed_hangul_latin, +5189,VIII-1_08-UB,existing_success,,existing_calibration,Standard,140322,S,40457,295.235,born-digital,lo,false,unknown, +5141,Structural Analysiss and Design of Process Equipment_00_Contents,existing_success,,existing_calibration,Reference,520220,S,31883,62.758,born-digital,lo,false,latin_dominant, +4815,Note_240830_소음진동교육 필기,controlled_backfill,,handwritten,Drawing,12659094,L,3524,0.285,scan-likely,hi,true,unknown, +4798,Note_240528_다이아프람워크숍,controlled_backfill,,handwritten,Note,236840,S,1030,4.453,scan-likely,hi,true,hangul_dominant, +4813,Note_240827_필기,controlled_backfill,,handwritten,Note,710770,S,43,0.062,scan-likely,hi,true,unknown, +5151,THE PIPE FABRICATORS BLUE BOOK,controlled_backfill,,scan_likely,Manual,40063084,L,136448,3.488,scan-likely,lo,true,unknown, +5268,황현필의 진보를 위한 역사_6장 제주4-3사건의 왜국을 멈추라,controlled_backfill,,scan_likely,Note,6188759,M,8746,1.447,scan-likely,lo,true,hangul_dominant, +5127,표준기계설계(KS)_08_핀,controlled_backfill,,scan_likely,Standard,6703655,M,10423,1.592,scan-likely,lo,true,unknown, +8855,2월 26일,controlled_backfill,,mixed,Report,121611,S,2048,17.245,mixed,lo,false,unknown, +4061,공업역학 동역학(제13판)_Chapter 14 질점의 운동역학_일과 에너지,controlled_backfill,,mixed,Academic_Paper,5850755,M,44811,7.843,mixed,lo,false,mixed_hangul_latin, +3782,"Safety and Health for Engineers_02_5 Local, International, and Voluntary Laws, Regulations, and Standards",controlled_backfill,,mixed,study_note,4822580,M,46808,9.939,mixed,lo,false,latin_dominant, +5179,Hydrogen-Embrittlement,controlled_backfill,,mixed,Reference,430502,S,9400,22.359,mixed,lo,false,mixed_hangul_latin, +5133,압력용기 핸드북_기타,controlled_backfill,,mixed,Reference,1813221,M,51754,29.228,mixed,lo,false,mixed_hangul_latin, +3757,Industrial Safety and Health Management(7-ED)_2 Development of the safety and Health Function,controlled_backfill,,born_digital,study_note,2849250,M,139372,50.089,born-digital,lo,false,latin_dominant, +3758,Industrial Safety and Health Management(7-ED)_3 Concepts of Hazard Avoidance,controlled_backfill,,born_digital,study_note,1506926,M,106008,72.036,born-digital,lo,false,latin_dominant, +5163,국내 지속가능경영보고서의 노동인권 분야에 대한 실태 분석,controlled_backfill,,born_digital,study_note,640161,S,54423,87.055,born-digital,lo,false,unknown, +5167,우리나라 기업의 환경정보 공시 현황과 제도적 개선방안,controlled_backfill,,born_digital,Academic_Paper,718354,S,44395,63.284,born-digital,lo,false,hangul_dominant, +5154,국내 금속가공 중소기업의 스마트팩토리 활용 정도에 대한 실증적 연구,controlled_backfill,,born_digital,Academic_Paper,257730,S,23896,94.942,born-digital,lo,false,mixed_hangul_latin, +5155,스마트 팩토리의 전략적 활용 연구,controlled_backfill,,born_digital,Academic_Paper,693276,S,75466,111.467,born-digital,lo,false,mixed_hangul_latin, +5137,Pressure Vessel Design Manual_01 General Topics,controlled_backfill,,born_digital,Manual,2421078,M,123902,52.405,born-digital,lo,false,latin_dominant, +5211,PTB-4-2013_00_Foreword,controlled_backfill,,born_digital,Standard,416742,S,29162,71.656,born-digital,lo,false,unknown, +5178,Hydrogen_Piping_and_Pipelines_ASME_Code,controlled_backfill,,born_digital,Standard,3131091,M,1162861,380.305,born-digital,lo,false,unknown, +5168,TCoYourPaperlessOffice-4.0,controlled_backfill,,born_digital,Manual,1526520,M,253397,169.98,born-digital,lo,false,unknown, +3765,Industrial Safety and Health Management(7-ED)_10 Environmental Control and Noise,controlled_backfill,,born_digital,study_note,1408586,M,80003,58.16,born-digital,lo,false,latin_dominant, +3769,Industrial Safety and Health Management(7-ED)_14 Materials Handling and Storage,controlled_backfill,,born_digital,study_note,1785536,M,91902,52.706,born-digital,lo,false,latin_dominant, +5274,황현필의 진보를 위한 역사_12장 대한민국의 정신을 훼손하지 말라,controlled_backfill,,large,Academic_Paper,14996152,L,35398,2.417,scan-likely,lo,true,hangul_dominant, +5180,ASME Sec I 2025,controlled_backfill,,large,Standard,14890413,L,1603702,110.285,born-digital,lo,false,unknown, diff --git a/scripts/phase1d_pilot.py b/scripts/phase1d_pilot.py index 2c43e18..75d5d45 100644 --- a/scripts/phase1d_pilot.py +++ b/scripts/phase1d_pilot.py @@ -4,26 +4,35 @@ * Phase 2 전체 백필 결정은 1D 결과 보고 후행. * 1B.5 (이미지 추출 / _meta 보존) 는 별도 PR — 본 스크립트 영역 아님. -Stratification: - ai_domain × file_size_bucket (page_count 는 documents 컬럼 없음 → file_size proxy) - 보조: 각 cell 안에서 file_size 작은/큰 mix. - document_type ∈ SKIP_DOC_TYPES 제외 (marker_worker 의 SKIP 룰과 동일). +Stratification (Round 2 refined, plan: ~/.claude/plans/stratified-mingling-otter.md): + 4 축: doc_type × file_size_band × text_density_band × handwritten_hint + + sample_source ∈ {existing_success, controlled_backfill} + - existing_success 5건 (anchor 1 + calibration 4) + - controlled_backfill 25건 (handwritten 3 / scan_likely 2~3 / mixed 5 / born_digital 12 / large 2) + + forced_include: doc 4809 (Note_240805_용접교육 필기) — known bad handwritten anchor. + document_type ∈ SKIP_DOC_TYPES 제외 (marker_worker 룰 미러). Subcommands: - select stratified 30건 dry-run + JSON 저장 - enqueue select 결과를 markdown 큐에 enqueue (uq_queue_active 위반 회피) - report md_status 분포·실패사유·quality 메트릭·UI 검수 URL 출력 + select stratified 30건 dry-run + CSV+JSON 저장 + enqueue select 결과를 markdown 큐에 enqueue (uq_queue_active 위반 회피) + report md_status 분포·실패사유·quality 메트릭·UI 검수 URL 출력 + eval_template pilot_1d_eval.csv 스켈레톤 출력 (사용자가 rubric 5축 점수 채움) 실행 (GPU 서버): - docker compose exec fastapi python /app/scripts/phase1d_pilot.py select + docker compose exec fastapi python /app/scripts/phase1d_pilot.py select \ + --csv /app/evals/markdown/pilot_1d_sample.csv docker compose exec fastapi python /app/scripts/phase1d_pilot.py enqueue --yes docker compose exec fastapi python /app/scripts/phase1d_pilot.py report + docker compose exec fastapi python /app/scripts/phase1d_pilot.py eval_template \ + --csv /app/evals/markdown/pilot_1d_eval.csv """ import argparse import asyncio +import csv import json import os +import random import re import sys from collections import Counter, defaultdict @@ -49,11 +58,43 @@ SIZE_BUCKETS = [ ("large", 5 * 1024 * 1024, 10**12), # > 5MB ] +# 4축 stratification 의 file_size_band — Round 2 plan +FILE_SIZE_BAND_THRESHOLDS = [ + ("S", 0, 1 * 1024 * 1024), # < 1MB + ("M", 1 * 1024 * 1024, 10 * 1024 * 1024), # 1~10MB + ("L", 10 * 1024 * 1024, 10**12), # > 10MB +] + +# text_density (chars per KB of file) — born-digital vs scan 구분 단일 깨끗한 proxy. +# 0.17 (필기 4809) ↔ 94 (born-digital 3759) 양 끝 검증됨. +TEXT_DENSITY_BANDS = [ + ("scan-likely", 0.0, 5.0), + ("mixed", 5.0, 50.0), + ("born-digital", 50.0, float("inf")), +] + +HANDWRITTEN_HINT_REGEX = re.compile(r"필기|노트|handwritten|scan|스캔|note", re.IGNORECASE) + +# Forced include — 사용자 시각 확인에서 발견된 known bad anchor. +# 1D 결과로 다음 라운드 튜닝 시 같은 문서를 재변환해 개선 여부 판정. +FORCED_INCLUDES: dict[int, str] = { + 4809: "known_bad_handwritten_anchor", +} + +# 재현성 시드 — 한 번 만든 sample CSV 가 동일 결과 보장. +SAMPLE_SEED = 20260502 + PILOT_TARGET = 30 +EXISTING_SUCCESS_TARGET = 5 +CONTROLLED_BACKFILL_TARGET = PILOT_TARGET - EXISTING_SUCCESS_TARGET # 25 + DEFAULT_OUT = Path("/tmp/phase1d_pilot.json") +DEFAULT_CSV = Path("/tmp/phase1d_pilot.csv") +DEFAULT_EVAL_CSV = Path("/tmp/phase1d_eval.csv") def _bucket(file_size: int | None) -> str: + """legacy 3-bucket — cmd_report 의 file_size bucket 호환.""" if file_size is None: return "unknown" for name, lo, hi in SIZE_BUCKETS: @@ -62,6 +103,111 @@ def _bucket(file_size: int | None) -> str: return "outlier" +def _file_size_band(file_size: int | None) -> str: + """Round 2 refined band: S / M / L.""" + if file_size is None: + return "unknown" + for name, lo, hi in FILE_SIZE_BAND_THRESHOLDS: + if lo <= file_size < hi: + return name + return "L" + + +def _text_density(text_len: int, file_size: int | None) -> float | None: + """chars per KB of file. file_size==0/None 이면 None.""" + if not file_size or file_size <= 0: + return None + return text_len / (file_size / 1024.0) + + +def _text_density_band(density: float | None) -> str: + if density is None: + return "unknown" + for name, lo, hi in TEXT_DENSITY_BANDS: + if lo <= density < hi: + return name + return "unknown" + + +def _handwritten_hint(title: str | None, file_path: str | None) -> str: + """title 또는 file_path 에 필기/노트/handwritten/scan 매칭 → 'hi' / 'lo'.""" + blob = " ".join(filter(None, [title or "", file_path or ""])) + return "hi" if HANDWRITTEN_HINT_REGEX.search(blob) else "lo" + + +def _scan_likely(text_len: int, file_size: int | None, density: float | None) -> bool: + """text_density < 5 또는 extracted_text 부재 → 스캔 가능성 높음.""" + if text_len == 0: + return True + if density is not None and density < 5.0: + return True + return False + + +def _script_mix(extracted_text: str | None, sample_chars: int = 10000) -> str: + """첫 N자에서 Hangul/CJK/Hiragana/Katakana/Latin 비율로 라벨링. + 한 script ≥ 0.7 → '