개발자 가이드
새로운 어노테이션 도구 컨버터를 추가하는 방법을 설명합니다.
아키텍처 개요
DM Schema 컨버터는 확장 가능한 프로세서 패턴을 사용합니다:
synapse_sdk/utils/converters/dm/
├── __init__.py # 공개 API
├── types.py # TypedDict 정의
├── base.py # BaseDMConverter 추상 클래스
├── from_v1.py # DMV1ToV2Converter
├── to_v1.py # DMV2ToV1Converter
├── utils.py # 유틸리티 함수
└── tools/
├── __init__.py # ToolProcessor Protocol
├── bounding_box.py # BoundingBoxProcessor
└── polygon.py # PolygonProcessor
ToolProcessor Protocol
새 도구 프로세서는 다음 Protocol을 구현해야 합니다:
from typing import Any, Protocol
class ToolProcessor(Protocol):
"""도구별 변환 프로세서 프로토콜"""
tool_name: str # 도구 이름 (예: "bounding_box", "polygon")
def to_v2(
self,
v1_annotation: dict[str, Any],
v1_data: dict[str, Any]
) -> dict[str, Any]:
"""V1 어노테이션을 V2로 변환"""
...
def to_v1(
self,
v2_annotation: dict[str, Any]
) -> tuple[dict[str, Any], dict[str, Any]]:
"""V2 어노테이션을 V1으로 변환
Returns:
(V1 annotation, V1 annotationData) 튜플
"""
...
새 도구 프로세서 추가
1단계: 프로세서 클래스 생성
synapse_sdk/utils/converters/dm/tools/ 디렉토리에 새 파일을 생성합니다.
예: keypoint.py
"""키포인트 도구 프로세서"""
from typing import Any
from ..utils import generate_random_id
class KeypointProcessor:
"""키포인트 도구 프로세서
V1 coordinate: {x, y}
V2 data: [x, y]
"""
tool_name = "keypoint"
def to_v2(
self,
v1_annotation: dict[str, Any],
v1_data: dict[str, Any]
) -> dict[str, Any]:
"""V1 키포인트를 V2로 변환"""
coordinate = v1_data.get("coordinate", {})
classification_obj = v1_annotation.get("classification") or {}
# V2 data: [x, y]
data = [
coordinate.get("x", 0),
coordinate.get("y", 0),
]
# V2 attrs 구성
attrs: list[dict[str, Any]] = []
for key, value in classification_obj.items():
if key != "class":
attrs.append({"name": key, "value": value})
return {
"id": v1_annotation.get("id", ""),
"classification": classification_obj.get("class", ""),
"attrs": attrs,
"data": data,
}
def to_v1(
self,
v2_annotation: dict[str, Any]
) -> tuple[dict[str, Any], dict[str, Any]]:
"""V2 키포인트를 V1으로 변환"""
annotation_id = v2_annotation.get("id", "")
classification_str = v2_annotation.get("classification", "")
attrs = v2_annotation.get("attrs", [])
data = v2_annotation.get("data", [0, 0])
# V1 coordinate 구성
coordinate = {
"x": data[0] if len(data) > 0 else 0,
"y": data[1] if len(data) > 1 else 0,
}
# V1 classification 구성
classification: dict[str, Any] = {"class": classification_str}
for attr in attrs:
name = attr.get("name", "")
value = attr.get("value")
if not name.startswith("_"):
classification[name] = value
v1_annotation = {
"id": annotation_id,
"tool": self.tool_name,
"classification": classification,
}
v1_data = {
"id": annotation_id,
"coordinate": coordinate,
}
return v1_annotation, v1_data
2단계: 프로세서 등록
from_v1.py와 to_v1.py의 _setup_tool_processors() 메서드에 등록합니다:
def _setup_tool_processors(self) -> None:
"""도구별 프로세서 등록"""
from .tools.bounding_box import BoundingBoxProcessor
from .tools.polygon import PolygonProcessor
from .tools.keypoint import KeypointProcessor # 새로 추가
self.register_processor(BoundingBoxProcessor())
self.register_processor(PolygonProcessor())
self.register_processor(KeypointProcessor()) # 새로 추가
3단계: 테스트 작성
tests/utils/converters/dm/ 디렉토리에 테스트 파일을 생성합니다:
"""키포인트 변환 테스트"""
import pytest
from synapse_sdk.utils.converters.dm import convert_v1_to_v2, convert_v2_to_v1
class TestV1ToV2Keypoint:
"""V1 → V2 키포인트 변환 테스트"""
@pytest.fixture
def v1_keypoint_sample(self):
return {
"annotations": {
"image_1": [
{
"id": "kp_1",
"tool": "keypoint",
"classification": {"class": "joint"}
}
]
},
"annotationsData": {
"image_1": [
{
"id": "kp_1",
"coordinate": {"x": 100, "y": 200}
}
]
}
}
def test_basic_conversion(self, v1_keypoint_sample):
"""기본 변환 테스트"""
result = convert_v1_to_v2(v1_keypoint_sample)
keypoint = result["annotation_data"]["images"][0]["keypoint"][0]
assert keypoint["data"] == [100, 200]
assert keypoint["classification"] == "joint"
def test_roundtrip(self, v1_keypoint_sample):
"""라운드트립 테스트"""
v2_result = convert_v1_to_v2(v1_keypoint_sample)
v1_result = convert_v2_to_v1(v2_result)
orig_coord = v1_keypoint_sample["annotationsData"]["image_1"][0]["coordinate"]
rest_coord = v1_result["annotationsData"]["image_1"][0]["coordinate"]
assert orig_coord["x"] == rest_coord["x"]
assert orig_coord["y"] == rest_coord["y"]
런타임 프로세서 등록
컨버터 인스턴스에 동적으로 프로세서를 등록할 수도 있습니다:
from synapse_sdk.utils.converters.dm.from_v1 import DMV1ToV2Converter
class CustomProcessor:
tool_name = "custom_tool"
def to_v2(self, v1_annotation, v1_data):
return {
"id": v1_annotation.get("id", ""),
"classification": v1_annotation.get("classification", {}).get("class", ""),
"attrs": [],
"data": v1_data.get("custom_data", {}),
}
def to_v1(self, v2_annotation):
return (
{
"id": v2_annotation.get("id", ""),
"tool": self.tool_name,
"classification": {"class": v2_annotation.get("classification", "")},
},
{
"id": v2_annotation.get("id", ""),
"custom_data": v2_annotation.get("data", {}),
},
)
# 커스텀 프로세서 등록
converter = DMV1ToV2Converter()
converter.register_processor(CustomProcessor())
# 변환 실행
result = converter.convert(v1_data_with_custom_tool)
유틸리티 함수
generate_random_id()
고유한 랜덤 ID를 생성합니다:
from synapse_sdk.utils.converters.dm.utils import generate_random_id
id1 = generate_random_id() # 예: "Cd1qfFQFI4"
id2 = generate_random_id() # 예: "AUjPgaMzQa"
extract_media_type_info(media_id)
미디어 ID에서 타입 정보를 추출합니다:
from synapse_sdk.utils.converters.dm.utils import extract_media_type_info
singular, plural = extract_media_type_info("image_1")
# singular: "image", plural: "images"
singular, plural = extract_media_type_info("video_5")
# singular: "video", plural: "videos"
타입 정의
주요 TypedDict 정의:
from synapse_sdk.utils.converters.dm.types import (
V2ConversionResult, # V1→V2 변환 결과
V2AnnotationData, # V2 공통 어노테이션 구조
AnnotationMeta, # V1 최상위 구조
)
테스트 가이드라인
- TDD 접근: 구현 전에 테스트를 먼저 작성합니다.
- 라운드트립 검증: V1→V2→V1 변환이 데이터를 보존하는지 확인합니다.
- 엣지 케이스: 빈 데이터, 누락된 필드 등을 테스트합니다.
- 기존 테스트 영향 없음: 새 프로세서 추가 후 기존 테스트가 여전히 통과하는지 확인합니다.
AI 어시스턴트용 프롬프트 템플릿
새로운 도구 타입을 추가할 때 아래 프롬프트를 사용하여 AI 어시스턴트에게 구현을 요청할 수 있습니다.
프롬프트 템플릿
# DM Schema Converter 새 도구 타입 추가 요청
## 도구 정보
- **도구 이름**: [도구 이름, 예: cuboid, ellipse, etc.]
- **미디어 타입**: [image / video / pcd / text / prompt]
## V1 데이터 구조
### annotations 구조
```json
{
"id": "[어노테이션 ID]",
"tool": "[도구 이름]",
"classification": {
"class": "[클래스명]",
"[추가 속성]": "[값]"
}
}
annotationsData 구조
{
"id": "[어노테이션 ID]",
"[데이터 필드명]": {
// V1 좌표/데이터 구조
}
}
V2 데이터 구조 (희망하는 형태)
{
"id": "[어노테이션 ID]",
"classification": "[클래스명]",
"attrs": [{"name": "[속성명]", "value": "[값]"}],
"data": [
// V2 좌표/데이터 구조 (배열 또는 객체)
]
}
변환 규칙
- V1
[필드명]→ V2data.[필드명] - V1
classification.class→ V2classification - V1
classification.[기타]→ V2attrs[{name, value}]
예시 데이터
V1 예시
// 실제 V1 데이터 예시
V2 예시 (변환 후)
// 실제 V2 데이터 예시
요청사항
synapse_sdk/utils/converters/dm/tools/[도구명].py프로세서 생성from_v1.py,to_v1.py에 프로세서 등록tests/utils/converters/dm/test_[도구명].py테스트 생성- (선택)
docs/docs/features/dm-schema-converter/[미디어]-[도구명].md문서 생성
### 사용 예시
아래는 실제 사용 예시입니다:
```markdown
# DM Schema Converter 새 도구 타입 추가 요청
## 도구 정보
- **도구 이름**: ellipse
- **미디어 타입**: image
## V1 데이터 구조
### annotations 구조
```json
{
"id": "ellipse_1",
"tool": "ellipse",
"classification": {
"class": "defect",
"severity": "high"
}
}
annotationsData 구조
{
"id": "ellipse_1",
"coordinate": {
"cx": 100,
"cy": 200,
"rx": 50,
"ry": 30,
"rotation": 45
}
}
V2 데이터 구조 (희망하는 형태)
{
"id": "ellipse_1",
"classification": "defect",
"attrs": [{"name": "severity", "value": "high"}],
"data": [100, 200, 50, 30, 45]
}
변환 규칙
- V1
coordinate.cx→ V2data[0] - V1
coordinate.cy→ V2data[1] - V1
coordinate.rx→ V2data[2] - V1
coordinate.ry→ V2data[3] - V1
coordinate.rotation→ V2data[4]
요청사항
- EllipseProcessor 프로세서 생성
- 컨버터에 프로세서 등록
- 테스트 케이스 작성
- 문서 생성
### 체크리스트
새 도구 타입 추가 시 확인해야 할 사항:
- [ ] `tools/[도구명].py` 프로세서 파일 생성
- [ ] `ToolProcessor` Protocol 구현 (`tool_name`, `to_v2`, `to_v1`)
- [ ] `from_v1.py`의 `_setup_tool_processors()`에 등록
- [ ] `to_v1.py`의 `_setup_tool_processors()`에 등록
- [ ] `tools/__init__.py`에 export 추가
- [ ] `tests/utils/converters/dm/test_[도구명].py` 테스트 작성
- [ ] V1→V2 변환 테스트
- [ ] V2→V1 변환 테스트
- [ ] 라운드트립 테스트 (V1→V2→V1)
- [ ] 기존 테스트 통과 확인 (`pytest tests/utils/converters/dm/`)
- [ ] (선택) 문서 작성 및 sidebars.ts 업데이트