Skip to content

Commit bc6cf05

Browse files
authored
Merge pull request #22 from Pseudo-Lab/feature/code-고도화-작업-issue
Feature/code 고도화 작업 issue
2 parents eabd526 + 0a6357d commit bc6cf05

File tree

36 files changed

+1446
-151
lines changed

36 files changed

+1446
-151
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ wheels/
1919
*.egg-info/
2020
.installed.cfg
2121
*.egg
22-
22+
data/*
2323
# Virtual Environment
2424
venv/
2525
env/

data_organizer.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import os
2+
import re
3+
import random
4+
from collections import defaultdict
5+
from pathlib import Path
6+
from typing import Dict, List, Tuple, Optional
7+
8+
class TableDataOrganizer:
9+
def __init__(self, data_root: str):
10+
"""
11+
Initialize the organizer with the root data directory.
12+
Args:
13+
data_root: Path to the 'data' directory.
14+
"""
15+
self.data_root = Path(data_root)
16+
self.grouped_data: Dict[str, List[str]] = defaultdict(list)
17+
self._organize_data()
18+
19+
def _organize_data(self):
20+
"""Scans the data directory and groups images by table ID."""
21+
# Regex to parse filenames: P_origin_{group}_{table}_{index}.png or P_origin_{group}_{table}.png
22+
# We want to group by "group_table"
23+
24+
# Pattern covers: group, table, index (optional)
25+
# e.g., P_origin_1_11_0.png -> group=1, table=11, index=0
26+
# e.g., P_origin_1_2.png -> group=1, table=2, index=-1 (conceptually)
27+
pattern = re.compile(r"P_origin_(\d+)_(\d+)(?:_(\d+))?\.png")
28+
29+
if not self.data_root.exists():
30+
print(f"Warning: Directory {self.data_root} does not exist.")
31+
return
32+
33+
for root, _, files in os.walk(self.data_root):
34+
for file in files:
35+
if not file.endswith(".png"):
36+
continue
37+
38+
match = pattern.match(file)
39+
if match:
40+
group_id = match.group(1)
41+
table_id = match.group(2)
42+
index = match.group(3)
43+
44+
# If index is missing (e.g. single file per table), treat as 0 or handle logically
45+
# For sorting purposes, we can treat None as -1 so it comes first, or just 0
46+
idx_val = int(index) if index is not None else -1
47+
48+
# Create a unique key for grouping: "group_{g}_table_{t}"
49+
key = f"P_origin_{group_id}_{table_id}"
50+
51+
abs_path = str(Path(root) / file)
52+
self.grouped_data[key].append((idx_val, abs_path))
53+
54+
# Sort each group by index
55+
for key in self.grouped_data:
56+
# Sort by index (tuple first element)
57+
self.grouped_data[key].sort(key=lambda x: x[0])
58+
# Keep only paths
59+
self.grouped_data[key] = [item[1] for item in self.grouped_data[key]]
60+
61+
def get_batches(self,
62+
sampling: bool = False,
63+
min_k: int = 2,
64+
max_k: int = 3,
65+
num_samples: int = 1) -> Dict[str, List[List[str]]]:
66+
"""
67+
Generates batches of images for each table.
68+
69+
Args:
70+
sampling: If True, randomly samples images. If False, returns all images as one batch.
71+
min_k: Minimum number of images to sample (inclusive, used if sampling=True).
72+
max_k: Maximum number of images to sample (inclusive, used if sampling=True).
73+
num_samples: Number of random batches to generate per table (used if sampling=True).
74+
75+
Returns:
76+
A dictionary where keys are table identifiers and values are LISTS of image lists (batches).
77+
e.g. {
78+
"P_origin_1_11": [ ["path/to/img0", "path/to/img2"] ]
79+
}
80+
"""
81+
results = {}
82+
83+
for key, images in self.grouped_data.items():
84+
if not sampling:
85+
# Return all images as a single batch
86+
results[key] = [images]
87+
else:
88+
table_batches = []
89+
n_images = len(images)
90+
91+
# If there are fewer images than min_k, we can't really "sample" between min_k and max_k
92+
# strictly unless we allow duplicates or just take what we have.
93+
# Logic: if n_images < min_k, just use all images once (effectively no sampling choice).
94+
effective_min = min(n_images, min_k)
95+
effective_max = min(n_images, max_k)
96+
97+
if n_images == 0:
98+
results[key] = []
99+
continue
100+
101+
for _ in range(num_samples):
102+
# Randomly choose k size
103+
# If effective_min == effective_max, then k is fixed
104+
k = random.randint(effective_min, effective_max) if effective_min <= effective_max else n_images
105+
106+
# Sample k images
107+
# Note: random.sample throws error if k > population
108+
# We guarded with min(), so k <= n_images
109+
if k > 0:
110+
batch = sorted(random.sample(images, k))
111+
table_batches.append(batch)
112+
else:
113+
# Should not happen typically unless file list is empty
114+
table_batches.append([])
115+
116+
results[key] = table_batches
117+
118+
return results
119+
120+
if __name__ == "__main__":
121+
# Test existing directory
122+
organizer = TableDataOrganizer("data")
123+
124+
print("=== Default Mode (All Images) ===")
125+
batches_default = organizer.get_batches(sampling=False)
126+
# Print first 2 keys
127+
for k in list(batches_default.keys())[:2]:
128+
print(f"Table: {k}")
129+
for batch in batches_default[k]:
130+
print(f" Batch size: {len(batch)}")
131+
# print(batch) # Uncomment to see paths
132+
133+
print("\n=== Sampling Mode (2-3 images) ===")
134+
batches_sampled = organizer.get_batches(sampling=True, min_k=2, max_k=3, num_samples=2)
135+
for k in list(batches_sampled.keys())[:2]:
136+
print(f"Table: {k}")
137+
for i, batch in enumerate(batches_sampled[k]):
138+
print(f" Sample {i+1}: size {len(batch)}")
139+
# print(batch) # Uncomment to see paths

fix1.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Fix 1: 데이터 구조화 및 랜덤 샘플링 (Data Organization & Random Sampling)
2+
3+
## 개요 (Overview)
4+
테이블 식별자를 기준으로 이미지를 그룹화하고, QA 생성을 위해 이미지의 일부를 무작위로 추출(샘플링)하는 로직을 구현했습니다. 이를 통해 동일한 테이블에 속한 이미지들의 다양한 조합을 사용하여 다채로운 QA 쌍을 생성할 수 있습니다.
5+
6+
## 새로운 파일: `data_organizer.py`
7+
이 스크립트는 `data` 디렉토리를 스캔하여 `P_origin_{group}_{table}_{index}.png` 명명 규칙에 따라 이미지를 그룹화합니다.
8+
9+
```python
10+
import os
11+
import re
12+
import random
13+
from collections import defaultdict
14+
from pathlib import Path
15+
from typing import Dict, List, Optional
16+
17+
class TableDataOrganizer:
18+
def __init__(self, data_root: str):
19+
self.data_root = Path(data_root)
20+
self.grouped_data: Dict[str, List[str]] = defaultdict(list)
21+
self._organize_data()
22+
23+
def _organize_data(self):
24+
pattern = re.compile(r"P_origin_(\d+)_(\d+)(?:_(\d+))?\.png")
25+
if not self.data_root.exists():
26+
return
27+
28+
for root, _, files in os.walk(self.data_root):
29+
for file in files:
30+
if not file.endswith(".png"): continue
31+
match = pattern.match(file)
32+
if match:
33+
group_id, table_id, index = match.group(1), match.group(2), match.group(3)
34+
idx_val = int(index) if index is not None else -1
35+
key = f"P_origin_{group_id}_{table_id}"
36+
abs_path = str(Path(root) / file)
37+
self.grouped_data[key].append((idx_val, abs_path))
38+
39+
for key in self.grouped_data:
40+
self.grouped_data[key].sort(key=lambda x: x[0])
41+
self.grouped_data[key] = [item[1] for item in self.grouped_data[key]]
42+
43+
def get_batches(self, sampling: bool = False, min_k: int = 2, max_k: int = 3, num_samples: int = 1) -> Dict[str, List[List[str]]]:
44+
results = {}
45+
for key, images in self.grouped_data.items():
46+
if not sampling:
47+
results[key] = [images]
48+
else:
49+
table_batches = []
50+
n_images = len(images)
51+
effective_min = min(n_images, min_k)
52+
effective_max = min(n_images, max_k)
53+
54+
if n_images == 0:
55+
results[key] = []
56+
continue
57+
58+
for _ in range(num_samples):
59+
k = random.randint(effective_min, effective_max) if effective_min <= effective_max else n_images
60+
if k > 0:
61+
batch = sorted(random.sample(images, k))
62+
table_batches.append(batch)
63+
results[key] = table_batches
64+
return results
65+
```
66+
67+
## `generate_synthetic_table/runner.py` 변경 사항
68+
- CLI 인자 추가: `--sampling`, `--min-k`, `--max-k`, `--num-samples`.
69+
- `TableDataOrganizer`를 통합하여 이미지 배치(batch)를 준비하도록 수정.
70+
- 단일 파일 대신 준비된 배치를 순회하며 실행하도록 루프 수정.
71+
72+
## `generate_synthetic_table/flow.py` 변경 사항
73+
- `TableState` 업데이트: `image_paths: List[str]` 필드 추가.
74+
- `generate_qa_from_image_node` 업데이트: 다중 이미지를 입력받아 LLM에 전달하도록 로직 수정.

fix2.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Fix 2: 도메인 맞춤형 프롬프트 (YAML 기반)
2+
3+
## 개요 (Overview)
4+
도메인별 최적화된 프롬프트 관리를 용이하게 하기 위해 기존 텍스트 파일 기반 시스템을 YAML 기반 시스템으로 교체했습니다. 이를 통해 입력 파일의 키나 CLI 인자를 기반으로 특정 도메인(예: 공공/정부 데이터)에 맞는 프롬프트를 자동으로 로드할 수 있습니다.
5+
6+
## 프롬프트 파일 (Prompt Files)
7+
- **`generate_synthetic_table/prompts/default.yaml`**: 파이프라인의 12개 이상의 단계에서 사용되는 기본 프롬프트들을 모두 포함합니다.
8+
- **`generate_synthetic_table/prompts/public.yaml`**: 도메인별 오버라이드 내용을 포함합니다.
9+
- 예시: `generate_qa_from_image` 프롬프트가 공공 부문 용어에 맞춰 커스터마이징되어 있습니다.
10+
11+
## `generate_synthetic_table/flow.py` 변경 사항
12+
- `_load_yaml_prompts` 함수 구현: YAML 파일 로드 및 캐싱 기능.
13+
- `_load_prompt(name, domain)` 함수 업데이트: `{domain}.yaml`을 먼저 확인하고, 없으면 `default.yaml`을 사용하도록 로직 변경 (Fallback).
14+
- 모든 노드(Node) 함수 업데이트:
15+
1. `TableState`에서 `domain` 정보를 읽어옴.
16+
2. 노드 실행 시 `_load_prompt(..., domain)`을 동적으로 호출하여 프롬프트 결정.
17+
18+
```python
19+
# flow.py 내 동적 로딩 예시
20+
def generate_qa_node(llm: ChatOpenAI) -> Callable[[TableState], TableState]:
21+
def _node(state: TableState) -> TableState:
22+
# state의 도메인 정보에 따라 프롬프트 로드
23+
prompt_template = _load_prompt("generate_qa", state.get("domain"))
24+
# ... 나머지 로직
25+
return _node
26+
```
27+
28+
## `generate_synthetic_table/runner.py` 변경 사항
29+
- CLI 인자 추가: `--domain`.
30+
- **자동 감지 로직 (Auto-Detection Logic)** 구현:
31+
- 입력 파일/폴더 이름이 `P_`로 시작하는 경우, 자동으로 `domain="public"`으로 설정.
32+
-`domain` 값을 `run_synthetic_table_flow`로 전달하여 `TableState`에 반영.
33+
34+
## 사용 방법 (Usage)
35+
`data/P_origin_1`과 같은 폴더를 처리할 때 시스템은 자동으로 'public' 도메인 프롬프트를 적용합니다.
36+
수동으로 지정할 수도 있습니다:
37+
```bash
38+
uv run python main.py data/MyTable --domain public
39+
```

0 commit comments

Comments
 (0)