@dssp/dcsp 1.0.2 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/bootstrap.js +13 -10
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/components/kpi-2d-lookup-chart.d.ts +50 -0
- package/dist-client/components/kpi-2d-lookup-chart.js +263 -0
- package/dist-client/components/kpi-2d-lookup-chart.js.map +1 -0
- package/dist-client/components/kpi-boxplot-chart.d.ts +31 -0
- package/dist-client/components/kpi-boxplot-chart.js +306 -0
- package/dist-client/components/kpi-boxplot-chart.js.map +1 -0
- package/dist-client/components/kpi-lookup-chart.d.ts +58 -0
- package/dist-client/components/kpi-lookup-chart.js +464 -0
- package/dist-client/components/kpi-lookup-chart.js.map +1 -0
- package/dist-client/components/kpi-mini-trend-chart.d.ts +14 -0
- package/dist-client/components/kpi-mini-trend-chart.js +180 -0
- package/dist-client/components/kpi-mini-trend-chart.js.map +1 -0
- package/dist-client/components/kpi-radar-chart.d.ts +17 -0
- package/dist-client/components/kpi-radar-chart.js +259 -0
- package/dist-client/components/kpi-radar-chart.js.map +1 -0
- package/dist-client/components/kpi-single-boxplot-chart.d.ts +36 -0
- package/dist-client/components/kpi-single-boxplot-chart.js +417 -0
- package/dist-client/components/kpi-single-boxplot-chart.js.map +1 -0
- package/dist-client/components/kpi-step-lookup-chart.d.ts +33 -0
- package/dist-client/components/kpi-step-lookup-chart.js +181 -0
- package/dist-client/components/kpi-step-lookup-chart.js.map +1 -0
- package/dist-client/components/kpi-trend-chart.d.ts +25 -0
- package/dist-client/components/kpi-trend-chart.js +241 -0
- package/dist-client/components/kpi-trend-chart.js.map +1 -0
- package/dist-client/google-map/common-google-map.d.ts +35 -0
- package/dist-client/google-map/common-google-map.js +349 -0
- package/dist-client/google-map/common-google-map.js.map +1 -0
- package/dist-client/google-map/google-map-loader.d.ts +6 -0
- package/dist-client/google-map/google-map-loader.js +23 -0
- package/dist-client/google-map/google-map-loader.js.map +1 -0
- package/dist-client/pages/kpi-admin/dssp-kpi-overview.d.ts +47 -0
- package/dist-client/pages/kpi-admin/dssp-kpi-overview.js +393 -0
- package/dist-client/pages/kpi-admin/dssp-kpi-overview.js.map +1 -0
- package/dist-client/pages/kpi-admin/kpi-system-guide.d.ts +18 -0
- package/dist-client/pages/kpi-admin/kpi-system-guide.js +696 -0
- package/dist-client/pages/kpi-admin/kpi-system-guide.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.d.ts +46 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js +945 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.d.ts +34 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js +238 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.d.ts +37 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js +689 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.d.ts +57 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js +866 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js.map +1 -0
- package/dist-client/pages/project-complete-tabs/pc-tab1-plan.d.ts +69 -0
- package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js +790 -0
- package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js.map +1 -0
- package/dist-client/pages/project-complete-tabs/pc-tab2-rating.d.ts +22 -0
- package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js +344 -0
- package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js.map +1 -0
- package/dist-client/pages/project-complete-tabs/pc-tab3-upload.d.ts +21 -0
- package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js +335 -0
- package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js.map +1 -0
- package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.d.ts +51 -0
- package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js +693 -0
- package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js.map +1 -0
- package/dist-client/pages/sv-project-complete.d.ts +31 -0
- package/dist-client/pages/sv-project-complete.js +364 -0
- package/dist-client/pages/sv-project-complete.js.map +1 -0
- package/dist-client/pages/sv-project-detail.d.ts +73 -0
- package/dist-client/pages/sv-project-detail.js +1387 -0
- package/dist-client/pages/sv-project-detail.js.map +1 -0
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +15 -0
- package/dist-client/route.js.map +1 -1
- package/dist-client/shared/complete-api.d.ts +45 -0
- package/dist-client/shared/complete-api.js +274 -0
- package/dist-client/shared/complete-api.js.map +1 -0
- package/dist-client/shared/func.d.ts +2 -0
- package/dist-client/shared/func.js +22 -0
- package/dist-client/shared/func.js.map +1 -0
- package/dist-client/shared/integration-fetch.d.ts +35 -0
- package/dist-client/shared/integration-fetch.js +53 -0
- package/dist-client/shared/integration-fetch.js.map +1 -0
- package/dist-client/shared/kpi-project-sync.d.ts +16 -0
- package/dist-client/shared/kpi-project-sync.js +129 -0
- package/dist-client/shared/kpi-project-sync.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/viewparts/menu-tools.d.ts +7 -0
- package/dist-client/viewparts/menu-tools.js +168 -21
- package/dist-client/viewparts/menu-tools.js.map +1 -1
- package/dist-server/index.d.ts +2 -0
- package/dist-server/index.js +5 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/middlewares/index.d.ts +7 -0
- package/dist-server/middlewares/index.js +100 -0
- package/dist-server/middlewares/index.js.map +1 -0
- package/dist-server/service/index.d.ts +4 -0
- package/dist-server/service/index.js +19 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/kpi-metric-value/index.d.ts +4 -0
- package/dist-server/service/kpi-metric-value/index.js +8 -0
- package/dist-server/service/kpi-metric-value/index.js.map +1 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.d.ts +198 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +1067 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.d.ts +15 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js +72 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js.map +1 -0
- package/dist-server/service/kpi-stat/index.d.ts +4 -0
- package/dist-server/service/kpi-stat/index.js +8 -0
- package/dist-server/service/kpi-stat/index.js.map +1 -0
- package/dist-server/service/kpi-stat/kpi-stat-query.d.ts +13 -0
- package/dist-server/service/kpi-stat/kpi-stat-query.js +751 -0
- package/dist-server/service/kpi-stat/kpi-stat-query.js.map +1 -0
- package/dist-server/service/kpi-stat/kpi-stat-types.d.ts +32 -0
- package/dist-server/service/kpi-stat/kpi-stat-types.js +126 -0
- package/dist-server/service/kpi-stat/kpi-stat-types.js.map +1 -0
- package/dist-server/service/kpi-value/index.d.ts +3 -0
- package/dist-server/service/kpi-value/index.js +7 -0
- package/dist-server/service/kpi-value/index.js.map +1 -0
- package/dist-server/service/kpi-value/kpi-value-query.d.ts +8 -0
- package/dist-server/service/kpi-value/kpi-value-query.js +90 -0
- package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/python/RUNNER_CONVERSION_GUIDE.md +81 -0
- package/python/doc_summary_engine_handoff/.env.example +21 -0
- package/python/doc_summary_engine_handoff/HANDOFF.md +266 -0
- package/python/doc_summary_engine_handoff/README.md +71 -0
- package/python/doc_summary_engine_handoff/__main__.py +159 -0
- package/python/doc_summary_engine_handoff/api.py +165 -0
- package/python/doc_summary_engine_handoff/extract.py +290 -0
- package/python/doc_summary_engine_handoff/requirements.txt +15 -0
- package/python/doc_summary_engine_handoff/summarize.py +247 -0
- package/python/inspection_ai_engine_handoff/.env.example +22 -0
- package/python/inspection_ai_engine_handoff/HANDOFF.md +541 -0
- package/python/inspection_ai_engine_handoff/README.md +83 -0
- package/python/inspection_ai_engine_handoff/__main__.py +169 -0
- package/python/inspection_ai_engine_handoff/activities.py +147 -0
- package/python/inspection_ai_engine_handoff/api.py +273 -0
- package/python/inspection_ai_engine_handoff/checklist.py +53 -0
- package/python/inspection_ai_engine_handoff/cv_handlers.py +46 -0
- package/python/inspection_ai_engine_handoff/cv_model_registry.py +252 -0
- package/python/inspection_ai_engine_handoff/cv_models/README.md +58 -0
- package/python/inspection_ai_engine_handoff/cv_models/_example_frame_alignment/handler.py +39 -0
- package/python/inspection_ai_engine_handoff/cv_models/_example_frame_alignment/model.json +22 -0
- package/python/inspection_ai_engine_handoff/engine.py +333 -0
- package/python/inspection_ai_engine_handoff/requirements.txt +12 -0
- package/python/inspection_ai_engine_handoff/router.py +200 -0
- package/python/inspection_ai_engine_handoff/shot_catalog.py +79 -0
- package/python/inspection_ai_engine_handoff/shot_planner.py +278 -0
- package/python/inspection_ai_engine_handoff/vlm.py +335 -0
- package/python/spec-matching-0.5.5/README.md +113 -0
- package/python/spec-matching-0.5.5/__main__.py +331 -0
- package/python/spec-matching-0.5.5/kcs.csv +6028 -0
- package/python/spec-matching-0.5.5/kcs_embeddings.meta.json +1 -0
- package/python/spec-matching-0.5.5/kcs_embeddings.npy +0 -0
- package/python/spec-matching-0.5.5/requirements.txt +3 -0
- package/python/spec-matching-0.5.5//342/230/2057 SF-TD4 /341/204/221/341/205/263/341/204/205/341/205/251/341/204/200/341/205/263/341/204/205/341/205/242/341/206/267/341/204/211/341/205/245/341/206/257/341/204/200/341/205/250/341/204/211/341/205/245 /341/204/213/341/205/243/341/206/274/341/204/211/341/205/265/341/206/250_V1.0.hwp +0 -0
- package/schema.graphql +1274 -22
- package/things-factory.config.js +7 -1
- package/translations/en.json +2 -0
- package/translations/ja.json +2 -0
- package/translations/ko.json +2 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""CV 모델 레지스트리 — 폴더 스캔 방식 (플러그인 구조).
|
|
2
|
+
|
|
3
|
+
단일 cv_models.json 을 손으로 편집하던 방식의 문제(충돌·전체 파손 위험)를 없애기 위해,
|
|
4
|
+
각 CV 모델을 **자기 폴더**에 담고 시스템이 그 폴더를 스캔해 레지스트리를 자동 조립합니다.
|
|
5
|
+
|
|
6
|
+
폴더 구조:
|
|
7
|
+
cv_models/
|
|
8
|
+
├── README.md
|
|
9
|
+
├── _example_xxx/ ← 이름이 '_' 또는 '.' 로 시작하면 스캔 제외(비활성/예시)
|
|
10
|
+
│ ├── model.json
|
|
11
|
+
│ └── handler.py
|
|
12
|
+
├── frame_alignment_v1/ ← 실제 모델 (폴더만 떨구면 자동 등록)
|
|
13
|
+
│ ├── model.json ← 메타데이터 (handler 필드 없음 — 규칙으로 자동 연결)
|
|
14
|
+
│ └── handler.py ← def run(images) -> CVResult
|
|
15
|
+
└── ...
|
|
16
|
+
|
|
17
|
+
규칙:
|
|
18
|
+
- 핸들러는 <폴더>/handler.py 의 run() 함수로 자동 연결 (model.json 에 경로 안 적음).
|
|
19
|
+
- handler.py 가 없으면 status='stub' 로 간주하고 'pending' 을 반환하는 기본 핸들러 사용.
|
|
20
|
+
(메타만 먼저 등록 → 나중에 코드 추가 가능)
|
|
21
|
+
- 폴더 하나가 깨져도(json 오류 등) 그 폴더만 건너뛰고 경고. 나머지는 정상 동작.
|
|
22
|
+
|
|
23
|
+
공개 API (engine/router/api 가 사용 — 시그니처 불변):
|
|
24
|
+
- load_registry() -> CVRegistry
|
|
25
|
+
- list_models() -> list[CVModelMeta]
|
|
26
|
+
- summarize_for_router() -> list[dict]
|
|
27
|
+
- resolve_handler(model_id) -> callable(images)->CVResult
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import importlib.util
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import sys
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Callable, List, Optional, Sequence
|
|
38
|
+
|
|
39
|
+
from PIL import Image
|
|
40
|
+
|
|
41
|
+
from cv_handlers import CVResult
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_REGISTRY_DIR = Path(
|
|
45
|
+
os.environ.get(
|
|
46
|
+
"CV_MODELS_DIR",
|
|
47
|
+
str(Path(__file__).with_name("cv_models")),
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
HANDLER_FUNC_NAME = "run" # 각 모델 폴더 handler.py 가 노출해야 하는 함수 이름
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _warn(msg: str) -> None:
|
|
55
|
+
print(f"[cv_model_registry] 경고: {msg}", file=sys.stderr)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class CVModelMeta:
|
|
60
|
+
"""라우터/UI 에 노출하는 CV 모델 메타데이터."""
|
|
61
|
+
model_id: str
|
|
62
|
+
name: str
|
|
63
|
+
description: str
|
|
64
|
+
applicable_to: list[str]
|
|
65
|
+
input_spec: dict
|
|
66
|
+
output_schema: dict
|
|
67
|
+
verdict_rules: dict
|
|
68
|
+
status: str # "stub" | "ready"
|
|
69
|
+
handler: str # 자동 연결된 handler.py 경로 (없으면 "")
|
|
70
|
+
version: str
|
|
71
|
+
|
|
72
|
+
def is_ready(self) -> bool:
|
|
73
|
+
return self.status == "ready"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class CVRegistry:
|
|
78
|
+
schema_version: str
|
|
79
|
+
description: str
|
|
80
|
+
models: list[CVModelMeta]
|
|
81
|
+
|
|
82
|
+
def by_id(self, model_id: str) -> Optional[CVModelMeta]:
|
|
83
|
+
for m in self.models:
|
|
84
|
+
if m.model_id == model_id:
|
|
85
|
+
return m
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# model.json 에서 반드시 있어야 하는 핵심 키 (나머지는 기본값으로 보완)
|
|
90
|
+
_REQUIRED_KEYS = {"model_id", "name", "description", "applicable_to"}
|
|
91
|
+
_VALID_STATUS = {"stub", "ready"}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _parse_model_json(data: dict, folder_name: str) -> dict:
|
|
95
|
+
"""model.json dict 검증 + 기본값 보완. 실패 시 ValueError."""
|
|
96
|
+
missing = _REQUIRED_KEYS - set(data.keys())
|
|
97
|
+
if missing:
|
|
98
|
+
raise ValueError(f"필수 키 누락: {sorted(missing)}")
|
|
99
|
+
if not isinstance(data["model_id"], str) or not data["model_id"].strip():
|
|
100
|
+
raise ValueError("model_id 가 비어있거나 문자열이 아님")
|
|
101
|
+
if not isinstance(data["applicable_to"], list):
|
|
102
|
+
raise ValueError("applicable_to 는 배열이어야 함")
|
|
103
|
+
status = data.get("status", "ready")
|
|
104
|
+
if status not in _VALID_STATUS:
|
|
105
|
+
raise ValueError(f"status 는 {_VALID_STATUS} 중 하나여야 함 (현재: {status})")
|
|
106
|
+
return {
|
|
107
|
+
"model_id": data["model_id"].strip(),
|
|
108
|
+
"name": str(data["name"]),
|
|
109
|
+
"description": str(data["description"]),
|
|
110
|
+
"applicable_to": [str(x) for x in data["applicable_to"]],
|
|
111
|
+
"input_spec": dict(data.get("input_spec", {})),
|
|
112
|
+
"output_schema": dict(data.get("output_schema", {})),
|
|
113
|
+
"verdict_rules": dict(data.get("verdict_rules", {})),
|
|
114
|
+
"status": status,
|
|
115
|
+
"version": str(data.get("version", "0.0.0")),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _iter_model_dirs(root: Path):
|
|
120
|
+
"""root 아래 모델 폴더들을 순회. '_' 또는 '.' 로 시작하는 폴더는 제외."""
|
|
121
|
+
if not root.exists() or not root.is_dir():
|
|
122
|
+
return
|
|
123
|
+
for child in sorted(root.iterdir(), key=lambda p: p.name):
|
|
124
|
+
if not child.is_dir():
|
|
125
|
+
continue
|
|
126
|
+
if child.name.startswith(("_", ".")):
|
|
127
|
+
continue
|
|
128
|
+
if (child / "model.json").exists():
|
|
129
|
+
yield child
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def load_registry(root: Optional[Path] = None) -> CVRegistry:
|
|
133
|
+
"""cv_models/ 폴더를 스캔해 레지스트리를 조립. 깨진 폴더는 건너뜀(경고)."""
|
|
134
|
+
root = Path(root) if root is not None else _REGISTRY_DIR
|
|
135
|
+
models: list[CVModelMeta] = []
|
|
136
|
+
seen_ids: set[str] = set()
|
|
137
|
+
|
|
138
|
+
for folder in _iter_model_dirs(root):
|
|
139
|
+
try:
|
|
140
|
+
raw = json.loads((folder / "model.json").read_text(encoding="utf-8"))
|
|
141
|
+
parsed = _parse_model_json(raw, folder.name)
|
|
142
|
+
except Exception as e:
|
|
143
|
+
_warn(f"모델 폴더 '{folder.name}' 의 model.json 로드 실패 → 건너뜀: {e}")
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
mid = parsed["model_id"]
|
|
147
|
+
if mid in seen_ids:
|
|
148
|
+
_warn(f"중복 model_id '{mid}' (폴더 '{folder.name}') → 건너뜀")
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
handler_py = folder / "handler.py"
|
|
152
|
+
has_handler = handler_py.exists()
|
|
153
|
+
status = parsed["status"]
|
|
154
|
+
if not has_handler:
|
|
155
|
+
# 코드가 아직 없으면 무조건 stub 취급
|
|
156
|
+
status = "stub"
|
|
157
|
+
|
|
158
|
+
seen_ids.add(mid)
|
|
159
|
+
models.append(CVModelMeta(
|
|
160
|
+
model_id=mid,
|
|
161
|
+
name=parsed["name"],
|
|
162
|
+
description=parsed["description"],
|
|
163
|
+
applicable_to=parsed["applicable_to"],
|
|
164
|
+
input_spec=parsed["input_spec"],
|
|
165
|
+
output_schema=parsed["output_schema"],
|
|
166
|
+
verdict_rules=parsed["verdict_rules"],
|
|
167
|
+
status=status,
|
|
168
|
+
handler=str(handler_py) if has_handler else "",
|
|
169
|
+
version=parsed["version"],
|
|
170
|
+
))
|
|
171
|
+
|
|
172
|
+
return CVRegistry(
|
|
173
|
+
schema_version="1.0",
|
|
174
|
+
description="cv_models/ 폴더에서 자동 조립된 CV 모델 레지스트리.",
|
|
175
|
+
models=models,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def list_models() -> list[CVModelMeta]:
|
|
180
|
+
return load_registry().models
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def summarize_for_router(models: Optional[Sequence[CVModelMeta]] = None) -> list[dict]:
|
|
184
|
+
"""LLM 라우터용 압축 뷰 (내부 필드 handler/version 제외)."""
|
|
185
|
+
if models is None:
|
|
186
|
+
models = list_models()
|
|
187
|
+
return [
|
|
188
|
+
{
|
|
189
|
+
"model_id": m.model_id,
|
|
190
|
+
"name": m.name,
|
|
191
|
+
"description": m.description,
|
|
192
|
+
"applicable_to": m.applicable_to,
|
|
193
|
+
"required_shots": m.input_spec.get("required_shots", []),
|
|
194
|
+
"output_keys": list(m.output_schema.keys()),
|
|
195
|
+
"status": m.status,
|
|
196
|
+
}
|
|
197
|
+
for m in models
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
CVHandlerFn = Callable[[Sequence[Image.Image]], CVResult]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _pending_handler(model_id: str) -> CVHandlerFn:
|
|
205
|
+
"""handler.py 가 없거나 stub 인 모델용 기본 핸들러 — 항상 pending."""
|
|
206
|
+
def run(images: Sequence[Image.Image]) -> CVResult:
|
|
207
|
+
return CVResult(
|
|
208
|
+
verdict="pending",
|
|
209
|
+
reason=f"CV 모델 '{model_id}' 핸들러 미구현 (handler.py 없음 또는 stub 단계).",
|
|
210
|
+
measurements={"stub": True, "n_images_received": len(images)},
|
|
211
|
+
used_images=[],
|
|
212
|
+
)
|
|
213
|
+
return run
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _import_run(handler_py: Path, model_id: str) -> CVHandlerFn:
|
|
217
|
+
"""<폴더>/handler.py 를 동적 import 해서 run() 을 반환."""
|
|
218
|
+
spec = importlib.util.spec_from_file_location(f"_cvmodel_{model_id}", handler_py)
|
|
219
|
+
if spec is None or spec.loader is None:
|
|
220
|
+
raise ImportError(f"{handler_py} import 스펙 생성 실패")
|
|
221
|
+
module = importlib.util.module_from_spec(spec)
|
|
222
|
+
spec.loader.exec_module(module)
|
|
223
|
+
fn = getattr(module, HANDLER_FUNC_NAME, None)
|
|
224
|
+
if fn is None:
|
|
225
|
+
raise AttributeError(f"{handler_py} 에 {HANDLER_FUNC_NAME}(images) 함수가 없습니다.")
|
|
226
|
+
return fn
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def resolve_handler(model_id: str) -> CVHandlerFn:
|
|
230
|
+
"""model_id → 실제 핸들러 callable. 핸들러 없으면 pending 핸들러 반환.
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
KeyError: 레지스트리에 model_id 없음.
|
|
234
|
+
"""
|
|
235
|
+
meta = load_registry().by_id(model_id)
|
|
236
|
+
if meta is None:
|
|
237
|
+
raise KeyError(f"CV 모델 레지스트리에 model_id 없음: {model_id}")
|
|
238
|
+
if not meta.handler:
|
|
239
|
+
return _pending_handler(model_id)
|
|
240
|
+
try:
|
|
241
|
+
return _import_run(Path(meta.handler), model_id)
|
|
242
|
+
except Exception as e:
|
|
243
|
+
_warn(f"모델 '{model_id}' handler.py 로드 실패 → pending 처리: {e}")
|
|
244
|
+
return _pending_handler(model_id)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
if __name__ == "__main__":
|
|
248
|
+
reg = load_registry()
|
|
249
|
+
print(f"CV 모델 레지스트리: 활성 {len(reg.models)}개")
|
|
250
|
+
for m in reg.models:
|
|
251
|
+
mark = "READY" if m.is_ready() else "stub "
|
|
252
|
+
print(f" [{mark}] {m.model_id} {m.name}")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# CV 모델 레지스트리 (폴더 방식)
|
|
2
|
+
|
|
3
|
+
이 폴더가 **CV 측정 모델 저장소**입니다. 모델마다 자기 폴더를 가지며, 시스템이
|
|
4
|
+
시작할 때 이 폴더를 스캔해 레지스트리를 자동으로 조립합니다. **공유 파일(JSON 하나)을
|
|
5
|
+
다 같이 편집하지 않으므로** 충돌·전체 파손 위험이 없습니다.
|
|
6
|
+
|
|
7
|
+
## 구조
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
cv_models/
|
|
11
|
+
├── README.md ← 이 문서
|
|
12
|
+
├── _example_frame_alignment/ ← 작성 예시 (이름이 '_' 로 시작 → 스캔 제외, 비활성)
|
|
13
|
+
│ ├── model.json
|
|
14
|
+
│ └── handler.py
|
|
15
|
+
└── <내모델_v1>/ ← 실제 모델 (이렇게 폴더만 두면 자동 등록)
|
|
16
|
+
├── model.json ← 모델 설명(메타데이터)
|
|
17
|
+
└── handler.py ← 모델 코드: def run(images) -> CVResult
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 모델 추가하는 법
|
|
21
|
+
|
|
22
|
+
1. `_example_frame_alignment/` 폴더를 통째로 복사해서 **'_' 없는** 이름으로 둡니다.
|
|
23
|
+
예: `cv_models/my_alignment_v1/`
|
|
24
|
+
2. `model.json` 의 값을 채웁니다 (아래 필드 설명).
|
|
25
|
+
3. `handler.py` 의 `run()` 안에 실제 측정 코드를 작성합니다.
|
|
26
|
+
4. `model.json` 의 `"status"` 를 `"ready"` 로 바꿉니다.
|
|
27
|
+
5. 서버 재시작 — 끝. 라우터가 자동으로 인식합니다.
|
|
28
|
+
|
|
29
|
+
> 코드가 아직 없으면 `model.json` 만 두어도 됩니다(`handler.py` 생략).
|
|
30
|
+
> 이 경우 자동으로 `stub` 처리되어 결과는 항상 `pending` 입니다.
|
|
31
|
+
> 메타만 먼저 등록하고 나중에 코드를 추가하는 흐름에 유용합니다.
|
|
32
|
+
|
|
33
|
+
## model.json 필드
|
|
34
|
+
|
|
35
|
+
| 필드 | 필수 | 설명 |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `model_id` | ✅ | 고유 ID(문자열). 예: `"frame_alignment_v1"` |
|
|
38
|
+
| `name` | ✅ | 사람이 읽는 이름 |
|
|
39
|
+
| `description` | ✅ | 무엇을 측정/검출하는지 1~2문장. **라우터가 검사항목 매칭에 사용** |
|
|
40
|
+
| `applicable_to` | ✅ | 적합한 검사 키워드 리스트. **라우터가 항목 텍스트와 비교** |
|
|
41
|
+
| `input_spec` | | `{required_shots, min_images, max_images, notes}`. `notes` 의 촬영요구는 촬영지시에 반영됨 |
|
|
42
|
+
| `output_schema` | | 산출 측정값 정의 `{ 이름: {type, desc} }` |
|
|
43
|
+
| `verdict_rules` | | 판정 기준(`tolerance`, `logic`). `logic` 은 사람용 문서, 실제 판정은 코드에서 |
|
|
44
|
+
| `status` | | `"stub"`(메타만/코드 미구현 → pending) 또는 `"ready"`(동작). 기본값 `ready` (단, handler.py 없으면 자동 stub) |
|
|
45
|
+
| `version` | | 버전 문자열. 기본값 `"0.0.0"` |
|
|
46
|
+
|
|
47
|
+
> `handler` 경로는 **적지 않습니다.** `<폴더>/handler.py:run` 으로 자동 연결됩니다.
|
|
48
|
+
|
|
49
|
+
## handler.py 규칙
|
|
50
|
+
|
|
51
|
+
- 함수 이름은 반드시 `run`.
|
|
52
|
+
- 시그니처: `run(images: Sequence[PIL.Image]) -> CVResult`.
|
|
53
|
+
- `CVResult` 는 `from cv_handlers import CVResult` 로 가져옵니다.
|
|
54
|
+
|
|
55
|
+
## 견고성
|
|
56
|
+
|
|
57
|
+
- 폴더 하나의 `model.json` 이 깨져도 그 폴더만 건너뛰고 경고를 출력합니다. 나머지 모델은 정상 동작.
|
|
58
|
+
- `_` 또는 `.` 로 시작하는 폴더는 스캔에서 제외됩니다(예시·보관용).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""예시 CV 핸들러 (작성 템플릿).
|
|
2
|
+
|
|
3
|
+
이 폴더는 이름이 '_' 로 시작하므로 레지스트리 스캔에서 제외됩니다(비활성).
|
|
4
|
+
실제 모델을 만들 때 이 폴더를 통째로 복사해 'cv_models/내모델_v1/' 처럼
|
|
5
|
+
'_' 없이 두고, 아래 run() 안을 채우세요.
|
|
6
|
+
|
|
7
|
+
규칙:
|
|
8
|
+
- 함수 이름은 반드시 run.
|
|
9
|
+
- 시그니처: run(images: Sequence[PIL.Image]) -> CVResult.
|
|
10
|
+
- 어떤 사진을 쓸지는 모델이 알아서 고름 (예: 풀샷만 사용).
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Sequence
|
|
15
|
+
|
|
16
|
+
from PIL import Image
|
|
17
|
+
|
|
18
|
+
from cv_handlers import CVResult
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run(images: Sequence[Image.Image]) -> CVResult:
|
|
22
|
+
# --- 실제 구현 가이드 ---
|
|
23
|
+
# 1) images 중 풀샷 선택
|
|
24
|
+
# 2) 직선 성분 검출 (Hough/RANSAC/딥러닝 등)
|
|
25
|
+
# 3) 수직/수평축 대비 편차각 측정
|
|
26
|
+
# 4) 허용오차(예: ±1.0°) 기준으로 verdict 결정
|
|
27
|
+
#
|
|
28
|
+
# 예시 반환값 (실제 측정값으로 교체):
|
|
29
|
+
return CVResult(
|
|
30
|
+
verdict="pending",
|
|
31
|
+
reason="예시 스텁 — 실제 구현 대기. run() 안을 채우세요.",
|
|
32
|
+
measurements={
|
|
33
|
+
"stub": True,
|
|
34
|
+
"n_images_received": len(images),
|
|
35
|
+
"expected_keys": ["vertical_deviation_deg", "horizontal_deviation_deg"],
|
|
36
|
+
},
|
|
37
|
+
used_images=[],
|
|
38
|
+
overlay_b64=None,
|
|
39
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"model_id": "frame_alignment_v1",
|
|
3
|
+
"name": "수직·수평 편차 측정 (작성 예시)",
|
|
4
|
+
"description": "풀샷에서 부재의 직선 성분을 검출해 수직축/수평축 대비 편차각을 측정. 시공 직선도/수직도 검사에 사용.",
|
|
5
|
+
"applicable_to": ["수직 상태", "수평 상태", "정렬", "기울기", "직선도"],
|
|
6
|
+
"input_spec": {
|
|
7
|
+
"required_shots": ["full_front"],
|
|
8
|
+
"min_images": 1,
|
|
9
|
+
"max_images": 3,
|
|
10
|
+
"notes": "부재 전체가 한 프레임에 들어와야 하고, 카메라 자체가 수평·수직으로 정렬되어 있어야 함."
|
|
11
|
+
},
|
|
12
|
+
"output_schema": {
|
|
13
|
+
"vertical_deviation_deg": { "type": "float", "desc": "절대 수직축 대비 편차(도)" },
|
|
14
|
+
"horizontal_deviation_deg": { "type": "float", "desc": "절대 수평축 대비 편차(도)" }
|
|
15
|
+
},
|
|
16
|
+
"verdict_rules": {
|
|
17
|
+
"tolerance": { "vertical_deviation_deg": 1.0, "horizontal_deviation_deg": 1.0 },
|
|
18
|
+
"logic": "두 편차 모두 tolerance 이내면 pass. 어느 하나라도 초과면 fail. 검출 선분이 너무 적으면 hold."
|
|
19
|
+
},
|
|
20
|
+
"status": "stub",
|
|
21
|
+
"version": "0.0.1"
|
|
22
|
+
}
|