이 문서는 tools/xdata/ 가 무엇을 어떤 책임 경계로 나누어 하는지 한 페이지로
설명한다. 세부 사용법은 xdata-guide.md, 경로 규약은
fs-path-convention.md 정본.
xvoice3 등 TTS 프로젝트의 학습 데이터는 다음 같은 사실을 동시에 가진다:
server_162:/HDD0/..., nfs_aihub:/HDD0/BACK_UP/AI_HUB_DATA/..., ...){lang}/{service}/{gender}/... vs AI_HUB {split}/source/{cat}/{leaf}/{speaker_code} 등)xdata 는 이 사실들을 단일 모델로 모아 두는 레이어다. 프로젝트 전체의 "지금 어떤 데이터가 어디에 있고 학습에 쓸 수 있는가" 를 한 곳에서 답한다.
Registry (project 단위)
├── Variant # natural_key 단위 = "이 화자/언어/스타일/소스 조합"
│ ├── inclusion_policy # default | opt_in | excluded
│ ├── tags / notes / derived_from
│ ├── variant_updated_at # variant 메타 (정책/태그/노트) 수정 시각
│ ├── variant_last_change_event_id # → audit 의 어느 sync_run/이벤트에서 수정됐는지
│ └── ProcessedVersion[] # 같은 natural_key 의 전처리 변형들
│ ├── version_key # phoneme/preprocessing chain 해시
│ ├── preprocessing axes (8개)
│ ├── data_source / experimental
│ ├── paths { server_tag → /abs/path }
│ ├── updated_at # 디스크 mtime
│ └── last_change_event_id # → audit 의 어느 sync_run 에서 변경됐는지
└── Speaker[] # 화자 메타 (현재 라이트한 사용)
natural_key (4-field, ADR 0007): {speaker_key}/{language}/{utterance_style}/{phoneme_source}
version_key (정본: tools/xdata/migrations/phase2_schema_v2.py::version_key):
{phoneme_encoding|raw}/{phoneme_source_version|unknown}/{breath}_{trim}_{sample_rate}_{sbert}_{perturbation}_{subset}_{experiment_tag}
┌─────────────────────────────────────┐
│ registry.metadata.sources[] │
│ { name, walker, root } │
│ (=학습 데이터의 등록된 출처 목록) │
└─────────────────┬───────────────────┘
│
┌──────────▼─────────┐
│ cmd_fs_sync │
│ (--all / --source)│
└──────────┬─────────┘
│ for each source:
▼
┌────────────────────┐
│ walker │ ← plugin (xvoice3_main / aihub_emotion / ...)
│ walk(root) │ 디스크 트리 → variant + pending
└──────────┬─────────┘
│
▼
┌─────────── 4-bucket diff ───────────┐
│ added / modified / removed / pending │
│ (vs registry 의 그 source 키) │
└──────────────────┬───────────────────┘
│
┌──────────────────┼─────────────────────┐
▼ ▼ ▼
┌────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ registry │ │ NocoDB │ │ audit │
│ .v3.json │ │ dataset_variants │ │ dataset_variant_ │
│ (snapshot) │ │ (primary) │ │ changes (event) │
└────────────┘ └──────────────────┘ └──────────────────┘
│
▼
sync_run row 1건/ run
(after.sources[] 에 source
별 카운트 분리. variant/pv
의 last_change_event_id 가
여기를 inverted-lookup)
같은 디스크 상태 + 같은 registry → diff 없음 → audit emit 없음 (make_sync_run_event 가 None 반환). 두 번 돌려도 NocoDB row, audit row, registry 변화 모두 없다.
| bucket | 의미 | 후속 |
|---|---|---|
added | 디스크에 있는데 registry 에 없음 | registry/NocoDB 신규 등록 |
modified | 양쪽에 있고 의미 있는 diff (_has_meaningful_diff) | registry/NocoDB upsert |
removed | registry 에 있는데 디스크에 없음 | registry/NocoDB 삭제 |
pending | walker 가 못 알아본 디렉토리 (unknown 토큰 등) | pending_<ts>.json 로 분리, 사람이 분류 후 walker 갱신 |
_has_meaningful_diff 는 audit 의 정본 필드 (VARIANT_TOP_FIELDS / PV_FIELDS) 만 비교 — last_change_event_id 같은 추적 메타 자체는 비교 대상에서 제외해서 idempotent 유지.
| 모듈 | 책임 | 책임 아닌 것 |
|---|---|---|
walkers/<NAME>.py | "이 디스크 트리를 어떻게 variant 들로 해석하나" — path 컨벤션 1개 = walker 1개 | reconcile, NocoDB, audit |
cli.py / cmd_fs_sync | walker 결과 vs registry 를 어떻게 reconcile 하나, 어떤 source 를 walk 할지 분기 | path 컨벤션 |
core.Registry | variants/speakers/metadata in-memory 모델 + JSON snapshot | NocoDB, audit, walker |
nocodb/writer.py | dataset_variants 로 fail-soft write-through (실패 시 spool) | reconcile |
audit.py | 변경 이벤트 (sync_run, variant_*, pv_*) 생성·diff·emit | reconcile, NocoDB primary 데이터 |
preprocessing.py | 디렉토리 suffix 토큰 → 전처리 축 매핑 정본 | path 컨벤션 (suffix 외 부분) |
walker 추가 = 파일 1개 신설 + register_walker 1줄. 다른 모듈 안 건드려도 새 데이터셋 컨벤션 흡수 가능.
registry.v3.json 의 metadata.sources[] 가 "이 registry 가 어떤 출처의 union 인가" 를 자기가 안다:
{
"metadata": {
"sources": [
{"name": "xvoice3_main",
"walker": "xvoice3_main",
"root": "server_162:/HDD0/TRAIN_DATA_S98_01"},
{"name": "aihub_emotion",
"walker": "aihub_emotion",
"root": "server_162:/HDD0/BACK_UP/AI_HUB_DATA/015.emotion_style00"}
]
}
}
운영 흐름:
xdata fs-sync --registry REG.json --all # 전 source
xdata fs-sync --registry REG.json --source NAME # 단일 source (실험)
xdata fs-sync --registry REG.json --root TAG:PATH # ad-hoc (deprecation)
cron / skill / 자동화는 모두 --all 한 줄에 의존. 새 데이터셋 추가 = registry.metadata.sources 에 한 줄 추가 + 필요시 walker 신설.
fs-sync 1회당 sync_run 이벤트 1 row. after.sources[] (list) 에 source 별 added/modified/removed/pending 카운트가 분해되어 들어간다 — "aihub_emotion 만 41 added 였다" 같은 분리 조회는 그 list 를 풀어 보면 답이 나온다.
variant/pv 의 last_change_event_id 는 sync_run row 의 event_id 를 가리킴 → "이 variant 가 마지막으로 어느 sync 에서 어떻게 바뀌었지?" 도 inverted-lookup 으로 답 가능.
데이터를 registry 에 넣는 두 갈래:
| 입구 | 무엇 | 언제 |
|---|---|---|
xdata parse | 외부 파일 (spk2path, context_csv) 기반 등록 | 새 프로젝트 부트스트랩, 외부 인덱스 import |
xdata fs-sync | 디스크 자체를 source-of-truth 로 reconcile | 정상 운영, 자동화, 실험 시작 시 |
xdata modify | 단일 variant 운영 메타 (inclusion_policy/notes) 수정 | 사람이 정책 결정 |
fs-sync 가 정상 운영 흐름의 정본이다. parse 는 부트스트랩 / legacy import 용.
새 데이터셋 컨벤션:
tools/xdata/walkers/{name}.py 작성, 모듈 import 시 register_walker(name, walk_fn) 호출registry.v3.json 의 metadata.sources 에 한 줄 추가xdata fs-sync --source name --dry-run 으로 검증 → --all 로 정상화새 utterance_style enum: NocoDB SingleSelect colOptions 에 추가 (tools/scripts/nocodb_add_style_options.py).
새 server: metadata.sources[] 에 root 가 다른 source 추가하면 됨. 같은 walker 가 여러 server 의 같은 컨벤션 처리.
dataset_variants 가 primary. 둘이 어긋나면 NocoDB 가 정본.make_sync_run_event() 가 변경 없는 sync (added/modified/removed 0) 에 None 을 반환해 emit 을 건너뛰는 데서 나온다 — event_id 자체는 timestamp 를 포함해 실행 시각이 다르면 항상 달라진다.~/.harness/spool/<table_id>.jsonl) 은 NocoDB 쓰기 실패의 fail-soft 큐. tools.integrations.nocodb.replay 로 재전송.pending_<ts>.json 은 walker 가 못 알아본 디렉토리들. 분류는 사람이 — walker 의 토큰/스타일 enum 갱신 → 다음 fs-sync 에서 자연 흡수.xdata-guide.md — CLI 사용법 / NocoDB 매핑 / 트러블슈팅fs-path-convention.md — xvoice3_main walker 의 path 규약 R1~R8nocodb-guide.md — NocoDB 베이스/테이블 구조