@things-factory/board-import 10.0.0-beta.93 → 10.0.0-y.0

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.
Files changed (45) hide show
  1. package/dist-server/service/extraction/contracts.d.ts +112 -0
  2. package/dist-server/service/extraction/contracts.js +3 -0
  3. package/dist-server/service/extraction/contracts.js.map +1 -0
  4. package/dist-server/service/extraction/index.d.ts +17 -0
  5. package/dist-server/service/extraction/index.js +62 -0
  6. package/dist-server/service/extraction/index.js.map +1 -0
  7. package/dist-server/service/extraction/label-scale-calibrator.d.ts +5 -0
  8. package/dist-server/service/extraction/label-scale-calibrator.js +111 -0
  9. package/dist-server/service/extraction/label-scale-calibrator.js.map +1 -0
  10. package/dist-server/service/extraction/layout-refiner.d.ts +16 -0
  11. package/dist-server/service/extraction/layout-refiner.js +88 -0
  12. package/dist-server/service/extraction/layout-refiner.js.map +1 -0
  13. package/dist-server/service/extraction/pipeline.d.ts +11 -0
  14. package/dist-server/service/extraction/pipeline.js +124 -0
  15. package/dist-server/service/extraction/pipeline.js.map +1 -0
  16. package/dist-server/service/extraction/source-router.d.ts +17 -0
  17. package/dist-server/service/extraction/source-router.js +40 -0
  18. package/dist-server/service/extraction/source-router.js.map +1 -0
  19. package/dist-server/service/extraction/stubs.d.ts +18 -0
  20. package/dist-server/service/extraction/stubs.js +38 -0
  21. package/dist-server/service/extraction/stubs.js.map +1 -0
  22. package/dist-server/service/extraction/vector-geometry.d.ts +7 -0
  23. package/dist-server/service/extraction/vector-geometry.js +21 -0
  24. package/dist-server/service/extraction/vector-geometry.js.map +1 -0
  25. package/dist-server/service/extraction/vlm-recognizer.d.ts +18 -0
  26. package/dist-server/service/extraction/vlm-recognizer.js +78 -0
  27. package/dist-server/service/extraction/vlm-recognizer.js.map +1 -0
  28. package/dist-server/service/index.d.ts +1 -0
  29. package/dist-server/service/index.js +4 -1
  30. package/dist-server/service/index.js.map +1 -1
  31. package/dist-server/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +6 -6
  33. package/probe/gcp-vision-probe.mjs +114 -0
  34. package/probe/opencv-geometry-probe.py +98 -0
  35. package/server/service/extraction/contracts.ts +142 -0
  36. package/server/service/extraction/extraction.test.ts +374 -0
  37. package/server/service/extraction/index.ts +51 -0
  38. package/server/service/extraction/label-scale-calibrator.ts +124 -0
  39. package/server/service/extraction/layout-refiner.ts +104 -0
  40. package/server/service/extraction/pipeline.ts +163 -0
  41. package/server/service/extraction/source-router.ts +45 -0
  42. package/server/service/extraction/stubs.ts +46 -0
  43. package/server/service/extraction/vector-geometry.ts +26 -0
  44. package/server/service/extraction/vlm-recognizer.ts +87 -0
  45. package/server/service/index.ts +4 -0
@@ -0,0 +1,112 @@
1
+ import type { UniversalEntity, Vec2, BoundingBox, AdapterInput, ParseResult } from '../types/index';
2
+ /** 입력 소스 분류 — 추출 경로 선택 기준. */
3
+ export type SourceKind = 'vector' | 'raster-topdown' | 'photo' | 'unknown';
4
+ /** 검출된 저수준 기하 프리미티브 (픽셀/소스 좌표계). */
5
+ export interface GeometricPrimitive {
6
+ kind: 'line' | 'rect' | 'region' | 'point';
7
+ bbox: BoundingBox;
8
+ /** line 일 때 양 끝점. */
9
+ segment?: {
10
+ a: Vec2;
11
+ b: Vec2;
12
+ };
13
+ /** 검출 신뢰도 0..1 (있으면). */
14
+ score?: number;
15
+ /**
16
+ * 이미 완전한 엔티티를 아는 추출기(벡터: DXF/IFC/SVG)가 원 엔티티를 그대로 실어 보낸다.
17
+ * 있으면 fuse 가 무손실로 사용(attribs/rotation/vertices 보존). CV 추출기는 비움 → fuse 가
18
+ * bbox 로 bare 엔티티 생성 후 Recognizer 가 의미 부여.
19
+ */
20
+ entity?: UniversalEntity;
21
+ /** 추출기별 원시 메타. */
22
+ meta?: Record<string, any>;
23
+ }
24
+ /** OCR 라벨 — 텍스트 + 위치 (식별/앵커/스케일 단서). */
25
+ export interface Label {
26
+ text: string;
27
+ bbox: BoundingBox;
28
+ score?: number;
29
+ }
30
+ /** 한 region(프리미티브/crop)에 대한 의미 인식 결과. */
31
+ export interface RecognitionResult {
32
+ /** 실제 보드 컴포넌트 type (카탈로그 그라운딩). 불명 시 undefined. */
33
+ componentType?: string;
34
+ confidence: number;
35
+ attribs?: Record<string, any>;
36
+ }
37
+ /** 스케일 캘리브레이션 결과 — 픽셀 → board 단위 변환. */
38
+ export interface ScaleResult {
39
+ unitsPerPixel: number;
40
+ origin: Vec2;
41
+ units?: ParseResult['units'];
42
+ }
43
+ /** 추출 컨텍스트 — 이미지 dims, 사용자 힌트, 카탈로그 등 (확장 여지). */
44
+ export interface ExtractionContext {
45
+ imageWidth?: number;
46
+ imageHeight?: number;
47
+ userPrompt?: string;
48
+ /** 카탈로그 그라운딩용 컴포넌트 type 목록 (Recognizer 가 제약). */
49
+ catalog?: string[];
50
+ /**
51
+ * 명시 스케일 기준 — 알려진 픽셀 span = 알려진 실제 길이 (예: UI 2점 캘리브레이션,
52
+ * 스케일바). ScaleCalibrator 가 최우선 사용. unitsPerPixel = units / pixels.
53
+ */
54
+ scaleReference?: {
55
+ pixels: number;
56
+ units: number;
57
+ unit?: string;
58
+ };
59
+ [k: string]: any;
60
+ }
61
+ export interface GeometryExtractor {
62
+ readonly id: string;
63
+ extract(input: AdapterInput, ctx?: ExtractionContext): Promise<GeometricPrimitive[]>;
64
+ }
65
+ export interface LabelExtractor {
66
+ readonly id: string;
67
+ extract(input: AdapterInput, ctx?: ExtractionContext): Promise<Label[]>;
68
+ }
69
+ export interface Recognizer {
70
+ readonly id: string;
71
+ /** region(프리미티브 + ctx)이 어떤 컴포넌트인지 — VLM 인식 전담(좌표 아님). */
72
+ recognize(region: GeometricPrimitive, ctx?: ExtractionContext): Promise<RecognitionResult>;
73
+ }
74
+ export interface ScaleCalibrator {
75
+ readonly id: string;
76
+ calibrate(labels: Label[], primitives: GeometricPrimitive[], ctx?: ExtractionContext): Promise<ScaleResult>;
77
+ }
78
+ export interface LayoutRefiner {
79
+ readonly id: string;
80
+ refine(entities: UniversalEntity[], ctx?: ExtractionContext): UniversalEntity[];
81
+ }
82
+ /** 한 시안(해석) — 공유 기하 위에 의미/그룹핑/전략만 달리한 결과. */
83
+ export interface VariantDraft {
84
+ /** URL 친화 short id (예: 'equipment-focus'). */
85
+ id: string;
86
+ /** 사용자 표시 라벨 (예: '장비 중심'). */
87
+ label: string;
88
+ description?: string;
89
+ entities: UniversalEntity[];
90
+ subjectName?: string;
91
+ subjectDescription?: string;
92
+ importStrategy?: string;
93
+ }
94
+ /**
95
+ * 시안 전략 — *정확한 공유 기하+라벨* 을 받아 N개 해석 시안을 만든다.
96
+ * (UX: 사용자가 3안 중 선택하는 흐름 보존. 기하는 공유라 셋 다 레이아웃 정상, 해석만 다름.)
97
+ * 미지정 시 파이프라인이 단일 default 시안 1개를 만든다.
98
+ */
99
+ export interface VariantStrategy {
100
+ readonly id: string;
101
+ interpret(primitives: GeometricPrimitive[], labels: Label[], set: ExtractorSet, ctx?: ExtractionContext): Promise<VariantDraft[]>;
102
+ }
103
+ /** 한 소스경로의 추출기 묶음 (geometry 필수, 나머지는 경로별 선택). */
104
+ export interface ExtractorSet {
105
+ geometry: GeometryExtractor;
106
+ labels?: LabelExtractor;
107
+ recognizer?: Recognizer;
108
+ scale?: ScaleCalibrator;
109
+ refiner?: LayoutRefiner;
110
+ /** 다중 시안 생성 전략 (3안 선택 UX). 미지정 시 단일 default 시안. */
111
+ variants?: VariantStrategy;
112
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=contracts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contracts.js","sourceRoot":"","sources":["../../../server/service/extraction/contracts.ts"],"names":[],"mappings":"","sourcesContent":["/*\n * 하이브리드 추출 contracts (Phase Board-AI §3.8) — 이미지/도면 → 레이아웃 추출을\n * *역할분담* 으로 분해하기 위한 provider-중립 인터페이스.\n *\n * 위치/기하 ← GeometryExtractor (벡터 파싱 > CV > VLM폴백)\n * 식별/라벨 ← LabelExtractor (OCR: GCP/Textract/Tesseract)\n * 의미/타입 ← Recognizer (VLM — region 인식 전담, 좌표 아님)\n * 스케일 ← ScaleCalibrator (치수/스케일바/알려진 크기)\n * 정리 ← LayoutRefiner (정렬/스냅/등간격/겹침해소)\n *\n * 구현체는 스캐폴딩 단계에선 stub. 본격 테스트 단계에서 소스 적합 impl 을 채운다.\n */\nimport type { UniversalEntity, Vec2, BoundingBox, AdapterInput, ParseResult } from '../types/index'\n\n/** 입력 소스 분류 — 추출 경로 선택 기준. */\nexport type SourceKind =\n | 'vector' // DXF/IFC/SVG/PDF-vector — 정밀 기하 직접 파싱\n | 'raster-topdown' // 평면도 비트맵 — CV + OCR + VLM 하이브리드\n | 'photo' // 사진/3D 렌더 — VLM 러프 폴백\n | 'unknown'\n\n/** 검출된 저수준 기하 프리미티브 (픽셀/소스 좌표계). */\nexport interface GeometricPrimitive {\n kind: 'line' | 'rect' | 'region' | 'point'\n bbox: BoundingBox\n /** line 일 때 양 끝점. */\n segment?: { a: Vec2; b: Vec2 }\n /** 검출 신뢰도 0..1 (있으면). */\n score?: number\n /**\n * 이미 완전한 엔티티를 아는 추출기(벡터: DXF/IFC/SVG)가 원 엔티티를 그대로 실어 보낸다.\n * 있으면 fuse 가 무손실로 사용(attribs/rotation/vertices 보존). CV 추출기는 비움 → fuse 가\n * bbox 로 bare 엔티티 생성 후 Recognizer 가 의미 부여.\n */\n entity?: UniversalEntity\n /** 추출기별 원시 메타. */\n meta?: Record<string, any>\n}\n\n/** OCR 라벨 — 텍스트 + 위치 (식별/앵커/스케일 단서). */\nexport interface Label {\n text: string\n bbox: BoundingBox\n score?: number\n}\n\n/** 한 region(프리미티브/crop)에 대한 의미 인식 결과. */\nexport interface RecognitionResult {\n /** 실제 보드 컴포넌트 type (카탈로그 그라운딩). 불명 시 undefined. */\n componentType?: string\n confidence: number\n attribs?: Record<string, any>\n}\n\n/** 스케일 캘리브레이션 결과 — 픽셀 → board 단위 변환. */\nexport interface ScaleResult {\n unitsPerPixel: number\n origin: Vec2\n units?: ParseResult['units']\n}\n\n/** 추출 컨텍스트 — 이미지 dims, 사용자 힌트, 카탈로그 등 (확장 여지). */\nexport interface ExtractionContext {\n imageWidth?: number\n imageHeight?: number\n userPrompt?: string\n /** 카탈로그 그라운딩용 컴포넌트 type 목록 (Recognizer 가 제약). */\n catalog?: string[]\n /**\n * 명시 스케일 기준 — 알려진 픽셀 span = 알려진 실제 길이 (예: UI 2점 캘리브레이션,\n * 스케일바). ScaleCalibrator 가 최우선 사용. unitsPerPixel = units / pixels.\n */\n scaleReference?: { pixels: number; units: number; unit?: string }\n [k: string]: any\n}\n\n// ── contracts ────────────────────────────────────────────────────────────────\n\nexport interface GeometryExtractor {\n readonly id: string\n extract(input: AdapterInput, ctx?: ExtractionContext): Promise<GeometricPrimitive[]>\n}\n\nexport interface LabelExtractor {\n readonly id: string\n extract(input: AdapterInput, ctx?: ExtractionContext): Promise<Label[]>\n}\n\nexport interface Recognizer {\n readonly id: string\n /** region(프리미티브 + ctx)이 어떤 컴포넌트인지 — VLM 인식 전담(좌표 아님). */\n recognize(region: GeometricPrimitive, ctx?: ExtractionContext): Promise<RecognitionResult>\n}\n\nexport interface ScaleCalibrator {\n readonly id: string\n calibrate(labels: Label[], primitives: GeometricPrimitive[], ctx?: ExtractionContext): Promise<ScaleResult>\n}\n\nexport interface LayoutRefiner {\n readonly id: string\n refine(entities: UniversalEntity[], ctx?: ExtractionContext): UniversalEntity[]\n}\n\n/** 한 시안(해석) — 공유 기하 위에 의미/그룹핑/전략만 달리한 결과. */\nexport interface VariantDraft {\n /** URL 친화 short id (예: 'equipment-focus'). */\n id: string\n /** 사용자 표시 라벨 (예: '장비 중심'). */\n label: string\n description?: string\n entities: UniversalEntity[]\n subjectName?: string\n subjectDescription?: string\n importStrategy?: string\n}\n\n/**\n * 시안 전략 — *정확한 공유 기하+라벨* 을 받아 N개 해석 시안을 만든다.\n * (UX: 사용자가 3안 중 선택하는 흐름 보존. 기하는 공유라 셋 다 레이아웃 정상, 해석만 다름.)\n * 미지정 시 파이프라인이 단일 default 시안 1개를 만든다.\n */\nexport interface VariantStrategy {\n readonly id: string\n interpret(\n primitives: GeometricPrimitive[],\n labels: Label[],\n set: ExtractorSet,\n ctx?: ExtractionContext\n ): Promise<VariantDraft[]>\n}\n\n/** 한 소스경로의 추출기 묶음 (geometry 필수, 나머지는 경로별 선택). */\nexport interface ExtractorSet {\n geometry: GeometryExtractor\n labels?: LabelExtractor\n recognizer?: Recognizer\n scale?: ScaleCalibrator\n refiner?: LayoutRefiner\n /** 다중 시안 생성 전략 (3안 선택 UX). 미지정 시 단일 default 시안. */\n variants?: VariantStrategy\n}\n"]}
@@ -0,0 +1,17 @@
1
+ export * from './contracts';
2
+ export * from './source-router';
3
+ export * from './pipeline';
4
+ export { VectorGeometryExtractor } from './vector-geometry';
5
+ export { GridSnapLayoutRefiner, type LayoutRefineOptions } from './layout-refiner';
6
+ export { VlmRecognizer } from './vlm-recognizer';
7
+ export { LabelScaleCalibrator } from './label-scale-calibrator';
8
+ export { CvGeometryExtractor, CloudOcrLabelExtractor, IdentityLayoutRefiner } from './stubs';
9
+ /**
10
+ * 기본 추출기 묶음 등록. *명시 호출 시에만* 등록 (side-effect 없음).
11
+ * - vector : VectorGeometryExtractor(실) + GridSnapLayoutRefiner(실) → 지금 동작.
12
+ * - raster-topdown / photo : VlmRecognizer·LabelScaleCalibrator·GridSnapLayoutRefiner 는 실,
13
+ * CV(geometry)/OCR 은 아직 stub. CvGeometry stub 이 먼저 throw 하므로 raster 경로 전체는
14
+ * CV 구현 후 동작.
15
+ * 본격 테스트 단계에서 남은 stub(CV/OCR)을 실 impl 로 교체.
16
+ */
17
+ export declare function registerDefaultExtractors(): void;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IdentityLayoutRefiner = exports.CloudOcrLabelExtractor = exports.CvGeometryExtractor = exports.LabelScaleCalibrator = exports.VlmRecognizer = exports.GridSnapLayoutRefiner = exports.VectorGeometryExtractor = void 0;
4
+ exports.registerDefaultExtractors = registerDefaultExtractors;
5
+ const tslib_1 = require("tslib");
6
+ /*
7
+ * 하이브리드 추출 모듈 (Phase Board-AI §3.8) — barrel.
8
+ *
9
+ * 순수 *추가* 모듈. 기존 어댑터/파이프라인(ImageAdapter, stage1-4)은 무변경.
10
+ * 본격 테스트 단계에서 나머지 stub → 실 impl 로 교체하고 ImageAdapter 를 흡수.
11
+ *
12
+ * 현재 실구현: VectorGeometryExtractor(DXF), GridSnapLayoutRefiner, VlmRecognizer(AI client),
13
+ * LabelScaleCalibrator. 아직 stub: CvGeometryExtractor, CloudOcrLabelExtractor.
14
+ */
15
+ tslib_1.__exportStar(require("./contracts"), exports);
16
+ tslib_1.__exportStar(require("./source-router"), exports);
17
+ tslib_1.__exportStar(require("./pipeline"), exports);
18
+ var vector_geometry_1 = require("./vector-geometry");
19
+ Object.defineProperty(exports, "VectorGeometryExtractor", { enumerable: true, get: function () { return vector_geometry_1.VectorGeometryExtractor; } });
20
+ var layout_refiner_1 = require("./layout-refiner");
21
+ Object.defineProperty(exports, "GridSnapLayoutRefiner", { enumerable: true, get: function () { return layout_refiner_1.GridSnapLayoutRefiner; } });
22
+ var vlm_recognizer_1 = require("./vlm-recognizer");
23
+ Object.defineProperty(exports, "VlmRecognizer", { enumerable: true, get: function () { return vlm_recognizer_1.VlmRecognizer; } });
24
+ var label_scale_calibrator_1 = require("./label-scale-calibrator");
25
+ Object.defineProperty(exports, "LabelScaleCalibrator", { enumerable: true, get: function () { return label_scale_calibrator_1.LabelScaleCalibrator; } });
26
+ var stubs_1 = require("./stubs");
27
+ Object.defineProperty(exports, "CvGeometryExtractor", { enumerable: true, get: function () { return stubs_1.CvGeometryExtractor; } });
28
+ Object.defineProperty(exports, "CloudOcrLabelExtractor", { enumerable: true, get: function () { return stubs_1.CloudOcrLabelExtractor; } });
29
+ Object.defineProperty(exports, "IdentityLayoutRefiner", { enumerable: true, get: function () { return stubs_1.IdentityLayoutRefiner; } });
30
+ const source_router_1 = require("./source-router");
31
+ const vector_geometry_2 = require("./vector-geometry");
32
+ const layout_refiner_2 = require("./layout-refiner");
33
+ const vlm_recognizer_2 = require("./vlm-recognizer");
34
+ const label_scale_calibrator_2 = require("./label-scale-calibrator");
35
+ const stubs_2 = require("./stubs");
36
+ /**
37
+ * 기본 추출기 묶음 등록. *명시 호출 시에만* 등록 (side-effect 없음).
38
+ * - vector : VectorGeometryExtractor(실) + GridSnapLayoutRefiner(실) → 지금 동작.
39
+ * - raster-topdown / photo : VlmRecognizer·LabelScaleCalibrator·GridSnapLayoutRefiner 는 실,
40
+ * CV(geometry)/OCR 은 아직 stub. CvGeometry stub 이 먼저 throw 하므로 raster 경로 전체는
41
+ * CV 구현 후 동작.
42
+ * 본격 테스트 단계에서 남은 stub(CV/OCR)을 실 impl 로 교체.
43
+ */
44
+ function registerDefaultExtractors() {
45
+ (0, source_router_1.registerExtractorSet)('vector', () => ({
46
+ geometry: new vector_geometry_2.VectorGeometryExtractor(),
47
+ refiner: new layout_refiner_2.GridSnapLayoutRefiner()
48
+ }));
49
+ (0, source_router_1.registerExtractorSet)('raster-topdown', () => ({
50
+ geometry: new stubs_2.CvGeometryExtractor(),
51
+ labels: new stubs_2.CloudOcrLabelExtractor(),
52
+ recognizer: new vlm_recognizer_2.VlmRecognizer(),
53
+ scale: new label_scale_calibrator_2.LabelScaleCalibrator(),
54
+ refiner: new layout_refiner_2.GridSnapLayoutRefiner()
55
+ }));
56
+ (0, source_router_1.registerExtractorSet)('photo', () => ({
57
+ geometry: new stubs_2.CvGeometryExtractor(),
58
+ recognizer: new vlm_recognizer_2.VlmRecognizer(),
59
+ refiner: new layout_refiner_2.GridSnapLayoutRefiner()
60
+ }));
61
+ }
62
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/extraction/index.ts"],"names":[],"mappings":";;;AAiCA,8DAiBC;;AAlDD;;;;;;;;GAQG;AACH,sDAA2B;AAC3B,0DAA+B;AAC/B,qDAA0B;AAC1B,qDAA2D;AAAlD,0HAAA,uBAAuB,OAAA;AAChC,mDAAkF;AAAzE,uHAAA,qBAAqB,OAAA;AAC9B,mDAAgD;AAAvC,+GAAA,aAAa,OAAA;AACtB,mEAA+D;AAAtD,8HAAA,oBAAoB,OAAA;AAC7B,iCAA4F;AAAnF,4GAAA,mBAAmB,OAAA;AAAE,+GAAA,sBAAsB,OAAA;AAAE,8GAAA,qBAAqB,OAAA;AAE3E,mDAAsD;AACtD,uDAA2D;AAC3D,qDAAwD;AACxD,qDAAgD;AAChD,qEAA+D;AAC/D,mCAAqE;AAErE;;;;;;;GAOG;AACH,SAAgB,yBAAyB;IACvC,IAAA,oCAAoB,EAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC;QACpC,QAAQ,EAAE,IAAI,yCAAuB,EAAE;QACvC,OAAO,EAAE,IAAI,sCAAqB,EAAE;KACrC,CAAC,CAAC,CAAA;IACH,IAAA,oCAAoB,EAAC,gBAAgB,EAAE,GAAG,EAAE,CAAC,CAAC;QAC5C,QAAQ,EAAE,IAAI,2BAAmB,EAAE;QACnC,MAAM,EAAE,IAAI,8BAAsB,EAAE;QACpC,UAAU,EAAE,IAAI,8BAAa,EAAE;QAC/B,KAAK,EAAE,IAAI,6CAAoB,EAAE;QACjC,OAAO,EAAE,IAAI,sCAAqB,EAAE;KACrC,CAAC,CAAC,CAAA;IACH,IAAA,oCAAoB,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;QACnC,QAAQ,EAAE,IAAI,2BAAmB,EAAE;QACnC,UAAU,EAAE,IAAI,8BAAa,EAAE;QAC/B,OAAO,EAAE,IAAI,sCAAqB,EAAE;KACrC,CAAC,CAAC,CAAA;AACL,CAAC","sourcesContent":["/*\n * 하이브리드 추출 모듈 (Phase Board-AI §3.8) — barrel.\n *\n * 순수 *추가* 모듈. 기존 어댑터/파이프라인(ImageAdapter, stage1-4)은 무변경.\n * 본격 테스트 단계에서 나머지 stub → 실 impl 로 교체하고 ImageAdapter 를 흡수.\n *\n * 현재 실구현: VectorGeometryExtractor(DXF), GridSnapLayoutRefiner, VlmRecognizer(AI client),\n * LabelScaleCalibrator. 아직 stub: CvGeometryExtractor, CloudOcrLabelExtractor.\n */\nexport * from './contracts'\nexport * from './source-router'\nexport * from './pipeline'\nexport { VectorGeometryExtractor } from './vector-geometry'\nexport { GridSnapLayoutRefiner, type LayoutRefineOptions } from './layout-refiner'\nexport { VlmRecognizer } from './vlm-recognizer'\nexport { LabelScaleCalibrator } from './label-scale-calibrator'\nexport { CvGeometryExtractor, CloudOcrLabelExtractor, IdentityLayoutRefiner } from './stubs'\n\nimport { registerExtractorSet } from './source-router'\nimport { VectorGeometryExtractor } from './vector-geometry'\nimport { GridSnapLayoutRefiner } from './layout-refiner'\nimport { VlmRecognizer } from './vlm-recognizer'\nimport { LabelScaleCalibrator } from './label-scale-calibrator'\nimport { CvGeometryExtractor, CloudOcrLabelExtractor } from './stubs'\n\n/**\n * 기본 추출기 묶음 등록. *명시 호출 시에만* 등록 (side-effect 없음).\n * - vector : VectorGeometryExtractor(실) + GridSnapLayoutRefiner(실) → 지금 동작.\n * - raster-topdown / photo : VlmRecognizer·LabelScaleCalibrator·GridSnapLayoutRefiner 는 실,\n * CV(geometry)/OCR 은 아직 stub. CvGeometry stub 이 먼저 throw 하므로 raster 경로 전체는\n * CV 구현 후 동작.\n * 본격 테스트 단계에서 남은 stub(CV/OCR)을 실 impl 로 교체.\n */\nexport function registerDefaultExtractors(): void {\n registerExtractorSet('vector', () => ({\n geometry: new VectorGeometryExtractor(),\n refiner: new GridSnapLayoutRefiner()\n }))\n registerExtractorSet('raster-topdown', () => ({\n geometry: new CvGeometryExtractor(),\n labels: new CloudOcrLabelExtractor(),\n recognizer: new VlmRecognizer(),\n scale: new LabelScaleCalibrator(),\n refiner: new GridSnapLayoutRefiner()\n }))\n registerExtractorSet('photo', () => ({\n geometry: new CvGeometryExtractor(),\n recognizer: new VlmRecognizer(),\n refiner: new GridSnapLayoutRefiner()\n }))\n}\n"]}
@@ -0,0 +1,5 @@
1
+ import type { ScaleCalibrator, ScaleResult, GeometricPrimitive, Label, ExtractionContext } from './contracts';
2
+ export declare class LabelScaleCalibrator implements ScaleCalibrator {
3
+ readonly id = "label-scale";
4
+ calibrate(labels: Label[], primitives: GeometricPrimitive[], ctx?: ExtractionContext): Promise<ScaleResult>;
5
+ }
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LabelScaleCalibrator = void 0;
4
+ const UNIT_TO_MM = { mm: 1, cm: 10, m: 1000, inch: 25.4 };
5
+ class LabelScaleCalibrator {
6
+ constructor() {
7
+ this.id = 'label-scale';
8
+ }
9
+ async calibrate(labels, primitives, ctx) {
10
+ const origin = layoutOrigin(primitives);
11
+ const dims = parseDimensions(labels);
12
+ const labelUnit = dominantUnit(dims);
13
+ // 1) 명시 reference 최우선.
14
+ const ref = ctx?.scaleReference;
15
+ if (ref && ref.pixels > 0 && Number.isFinite(ref.units)) {
16
+ return {
17
+ unitsPerPixel: ref.units / ref.pixels,
18
+ origin,
19
+ units: normUnit(ref.unit) ?? labelUnit ?? 'unknown'
20
+ };
21
+ }
22
+ // 2) 치수 라벨 휴리스틱.
23
+ const extent = layoutExtentPx(primitives);
24
+ if (dims.length > 0 && extent > 0 && labelUnit) {
25
+ const maxMm = Math.max(...dims.map(d => d.value * UNIT_TO_MM[d.unit]));
26
+ const mmPerPixel = maxMm / extent;
27
+ return { unitsPerPixel: mmPerPixel / UNIT_TO_MM[labelUnit], origin, units: labelUnit };
28
+ }
29
+ // 3) 폴백.
30
+ return { unitsPerPixel: 1, origin, units: labelUnit ?? 'unknown' };
31
+ }
32
+ }
33
+ exports.LabelScaleCalibrator = LabelScaleCalibrator;
34
+ // ── helpers ──────────────────────────────────────────────────────────────────
35
+ function layoutOrigin(primitives) {
36
+ if (!primitives.length)
37
+ return { x: 0, y: 0 };
38
+ let minX = Infinity;
39
+ let minY = Infinity;
40
+ for (const p of primitives) {
41
+ if (p.bbox.min.x < minX)
42
+ minX = p.bbox.min.x;
43
+ if (p.bbox.min.y < minY)
44
+ minY = p.bbox.min.y;
45
+ }
46
+ return { x: minX, y: minY };
47
+ }
48
+ function layoutExtentPx(primitives) {
49
+ if (!primitives.length)
50
+ return 0;
51
+ let minX = Infinity;
52
+ let minY = Infinity;
53
+ let maxX = -Infinity;
54
+ let maxY = -Infinity;
55
+ for (const p of primitives) {
56
+ minX = Math.min(minX, p.bbox.min.x);
57
+ minY = Math.min(minY, p.bbox.min.y);
58
+ maxX = Math.max(maxX, p.bbox.max.x);
59
+ maxY = Math.max(maxY, p.bbox.max.y);
60
+ }
61
+ return Math.max(maxX - minX, maxY - minY);
62
+ }
63
+ /** "5000mm" / "12.5 m" / '300cm' 같은 치수 라벨 파싱 (단위 필수 — 무단위 숫자는 무시). */
64
+ function parseDimensions(labels) {
65
+ const out = [];
66
+ for (const l of labels) {
67
+ const m = /^\s*(\d+(?:[.,]\d+)?)\s*(mm|cm|m|in|inch|")\s*$/i.exec(l.text ?? '');
68
+ if (!m)
69
+ continue;
70
+ const value = parseFloat(m[1].replace(',', '.'));
71
+ if (!Number.isFinite(value) || value <= 0)
72
+ continue;
73
+ const u = normUnit(m[2]);
74
+ if (u)
75
+ out.push({ value, unit: u });
76
+ }
77
+ return out;
78
+ }
79
+ /** 가장 빈번한 단위 (동률이면 먼저 본 것). */
80
+ function dominantUnit(dims) {
81
+ if (!dims.length)
82
+ return undefined;
83
+ const count = new Map();
84
+ for (const d of dims)
85
+ count.set(d.unit, (count.get(d.unit) ?? 0) + 1);
86
+ let best;
87
+ let bestN = 0;
88
+ for (const d of dims) {
89
+ const n = count.get(d.unit);
90
+ if (n > bestN) {
91
+ bestN = n;
92
+ best = d.unit;
93
+ }
94
+ }
95
+ return best;
96
+ }
97
+ function normUnit(u) {
98
+ if (!u)
99
+ return undefined;
100
+ const s = u.toLowerCase();
101
+ if (s === 'mm')
102
+ return 'mm';
103
+ if (s === 'cm')
104
+ return 'cm';
105
+ if (s === 'm')
106
+ return 'm';
107
+ if (s === 'in' || s === 'inch' || s === '"')
108
+ return 'inch';
109
+ return undefined;
110
+ }
111
+ //# sourceMappingURL=label-scale-calibrator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"label-scale-calibrator.js","sourceRoot":"","sources":["../../../server/service/extraction/label-scale-calibrator.ts"],"names":[],"mappings":";;;AAeA,MAAM,UAAU,GAAyB,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;AAO/E,MAAa,oBAAoB;IAAjC;QACW,OAAE,GAAG,aAAa,CAAA;IAgC7B,CAAC;IA9BC,KAAK,CAAC,SAAS,CACb,MAAe,EACf,UAAgC,EAChC,GAAuB;QAEvB,MAAM,MAAM,GAAG,YAAY,CAAC,UAAU,CAAC,CAAA;QACvC,MAAM,IAAI,GAAG,eAAe,CAAC,MAAM,CAAC,CAAA;QACpC,MAAM,SAAS,GAAG,YAAY,CAAC,IAAI,CAAC,CAAA;QAEpC,uBAAuB;QACvB,MAAM,GAAG,GAAG,GAAG,EAAE,cAAc,CAAA;QAC/B,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YACxD,OAAO;gBACL,aAAa,EAAE,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,MAAM;gBACrC,MAAM;gBACN,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,SAAS,IAAI,SAAS;aACpD,CAAA;QACH,CAAC;QAED,iBAAiB;QACjB,MAAM,MAAM,GAAG,cAAc,CAAC,UAAU,CAAC,CAAA;QACzC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,GAAG,CAAC,IAAI,SAAS,EAAE,CAAC;YAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACtE,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,CAAA;YACjC,OAAO,EAAE,aAAa,EAAE,UAAU,GAAG,UAAU,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,CAAA;QACxF,CAAC;QAED,SAAS;QACT,OAAO,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,IAAI,SAAS,EAAE,CAAA;IACpE,CAAC;CACF;AAjCD,oDAiCC;AAED,gFAAgF;AAChF,SAAS,YAAY,CAAC,UAAgC;IACpD,IAAI,CAAC,UAAU,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAA;IAC7C,IAAI,IAAI,GAAG,QAAQ,CAAA;IACnB,IAAI,IAAI,GAAG,QAAQ,CAAA;IACnB,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI;YAAE,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;QAC5C,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI;YAAE,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;IAC9C,CAAC;IACD,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAA;AAC7B,CAAC;AAED,SAAS,cAAc,CAAC,UAAgC;IACtD,IAAI,CAAC,UAAU,CAAC,MAAM;QAAE,OAAO,CAAC,CAAA;IAChC,IAAI,IAAI,GAAG,QAAQ,CAAA;IACnB,IAAI,IAAI,GAAG,QAAQ,CAAA;IACnB,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAA;IACpB,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAA;IACpB,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QACnC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QACnC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QACnC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;IACrC,CAAC;IACD,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC,CAAA;AAC3C,CAAC;AAED,sEAAsE;AACtE,SAAS,eAAe,CAAC,MAAe;IACtC,MAAM,GAAG,GAAU,EAAE,CAAA;IACrB,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,MAAM,CAAC,GAAG,kDAAkD,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAA;QAC/E,IAAI,CAAC,CAAC;YAAE,SAAQ;QAChB,MAAM,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;QAChD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC;YAAE,SAAQ;QACnD,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACxB,IAAI,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAA;IACrC,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,+BAA+B;AAC/B,SAAS,YAAY,CAAC,IAAW;IAC/B,IAAI,CAAC,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAA;IAClC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAgB,CAAA;IACrC,KAAK,MAAM,CAAC,IAAI,IAAI;QAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IACrE,IAAI,IAAsB,CAAA;IAC1B,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,MAAM,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAE,CAAA;QAC5B,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC;YACd,KAAK,GAAG,CAAC,CAAA;YACT,IAAI,GAAG,CAAC,CAAC,IAAI,CAAA;QACf,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,QAAQ,CAAC,CAAU;IAC1B,IAAI,CAAC,CAAC;QAAE,OAAO,SAAS,CAAA;IACxB,MAAM,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAA;IACzB,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IAC3B,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IAC3B,IAAI,CAAC,KAAK,GAAG;QAAE,OAAO,GAAG,CAAA;IACzB,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,GAAG;QAAE,OAAO,MAAM,CAAA;IAC1D,OAAO,SAAS,CAAA;AAClB,CAAC","sourcesContent":["/*\n * LabelScaleCalibrator — 픽셀 좌표를 실제 단위로 변환할 스케일을 도출 (Phase Board-AI §3.8).\n *\n * 우선순위:\n * 1) 명시 reference (ctx.scaleReference: 알려진 픽셀 span = 실제 길이) — 가장 정확.\n * 2) 치수 라벨 휴리스틱 — 가장 큰 치수값(mm 환산) ÷ 레이아웃 최대 픽셀 범위. ballpark.\n * (정밀 px↔real 페어링엔 치수선 기하가 필요 — 추후. 여기선 근사.)\n * 3) 폴백 — 항등 스케일(unitsPerPixel=1), 단위는 라벨에서 추론 or 'unknown'.\n *\n * origin 은 항상 프리미티브 묶음의 좌상단(min 코너) → 좌표를 0 기준 상대화.\n * 순수·결정적 (외부 호출 없음).\n */\nimport type { ScaleCalibrator, ScaleResult, GeometricPrimitive, Label, ExtractionContext } from './contracts'\n\ntype Unit = 'mm' | 'cm' | 'm' | 'inch'\nconst UNIT_TO_MM: Record<Unit, number> = { mm: 1, cm: 10, m: 1000, inch: 25.4 }\n\ninterface Dim {\n value: number\n unit: Unit\n}\n\nexport class LabelScaleCalibrator implements ScaleCalibrator {\n readonly id = 'label-scale'\n\n async calibrate(\n labels: Label[],\n primitives: GeometricPrimitive[],\n ctx?: ExtractionContext\n ): Promise<ScaleResult> {\n const origin = layoutOrigin(primitives)\n const dims = parseDimensions(labels)\n const labelUnit = dominantUnit(dims)\n\n // 1) 명시 reference 최우선.\n const ref = ctx?.scaleReference\n if (ref && ref.pixels > 0 && Number.isFinite(ref.units)) {\n return {\n unitsPerPixel: ref.units / ref.pixels,\n origin,\n units: normUnit(ref.unit) ?? labelUnit ?? 'unknown'\n }\n }\n\n // 2) 치수 라벨 휴리스틱.\n const extent = layoutExtentPx(primitives)\n if (dims.length > 0 && extent > 0 && labelUnit) {\n const maxMm = Math.max(...dims.map(d => d.value * UNIT_TO_MM[d.unit]))\n const mmPerPixel = maxMm / extent\n return { unitsPerPixel: mmPerPixel / UNIT_TO_MM[labelUnit], origin, units: labelUnit }\n }\n\n // 3) 폴백.\n return { unitsPerPixel: 1, origin, units: labelUnit ?? 'unknown' }\n }\n}\n\n// ── helpers ──────────────────────────────────────────────────────────────────\nfunction layoutOrigin(primitives: GeometricPrimitive[]): { x: number; y: number } {\n if (!primitives.length) return { x: 0, y: 0 }\n let minX = Infinity\n let minY = Infinity\n for (const p of primitives) {\n if (p.bbox.min.x < minX) minX = p.bbox.min.x\n if (p.bbox.min.y < minY) minY = p.bbox.min.y\n }\n return { x: minX, y: minY }\n}\n\nfunction layoutExtentPx(primitives: GeometricPrimitive[]): number {\n if (!primitives.length) return 0\n let minX = Infinity\n let minY = Infinity\n let maxX = -Infinity\n let maxY = -Infinity\n for (const p of primitives) {\n minX = Math.min(minX, p.bbox.min.x)\n minY = Math.min(minY, p.bbox.min.y)\n maxX = Math.max(maxX, p.bbox.max.x)\n maxY = Math.max(maxY, p.bbox.max.y)\n }\n return Math.max(maxX - minX, maxY - minY)\n}\n\n/** \"5000mm\" / \"12.5 m\" / '300cm' 같은 치수 라벨 파싱 (단위 필수 — 무단위 숫자는 무시). */\nfunction parseDimensions(labels: Label[]): Dim[] {\n const out: Dim[] = []\n for (const l of labels) {\n const m = /^\\s*(\\d+(?:[.,]\\d+)?)\\s*(mm|cm|m|in|inch|\")\\s*$/i.exec(l.text ?? '')\n if (!m) continue\n const value = parseFloat(m[1].replace(',', '.'))\n if (!Number.isFinite(value) || value <= 0) continue\n const u = normUnit(m[2])\n if (u) out.push({ value, unit: u })\n }\n return out\n}\n\n/** 가장 빈번한 단위 (동률이면 먼저 본 것). */\nfunction dominantUnit(dims: Dim[]): Unit | undefined {\n if (!dims.length) return undefined\n const count = new Map<Unit, number>()\n for (const d of dims) count.set(d.unit, (count.get(d.unit) ?? 0) + 1)\n let best: Unit | undefined\n let bestN = 0\n for (const d of dims) {\n const n = count.get(d.unit)!\n if (n > bestN) {\n bestN = n\n best = d.unit\n }\n }\n return best\n}\n\nfunction normUnit(u?: string): Unit | undefined {\n if (!u) return undefined\n const s = u.toLowerCase()\n if (s === 'mm') return 'mm'\n if (s === 'cm') return 'cm'\n if (s === 'm') return 'm'\n if (s === 'in' || s === 'inch' || s === '\"') return 'inch'\n return undefined\n}\n"]}
@@ -0,0 +1,16 @@
1
+ import type { UniversalEntity } from '../types/index';
2
+ import type { LayoutRefiner, ExtractionContext } from './contracts';
3
+ export interface LayoutRefineOptions {
4
+ /** 근접 좌표 정렬 (default true). */
5
+ align?: boolean;
6
+ /** 정렬 클러스터 허용오차. 미지정 시 엔티티 크기 중앙값으로 자동. */
7
+ alignTolerance?: number;
8
+ /** >0 이면 위치를 이 step 으로 스냅. */
9
+ grid?: number;
10
+ }
11
+ export declare class GridSnapLayoutRefiner implements LayoutRefiner {
12
+ private readonly opts;
13
+ readonly id = "grid-snap";
14
+ constructor(opts?: LayoutRefineOptions);
15
+ refine(entities: UniversalEntity[], _ctx?: ExtractionContext): UniversalEntity[];
16
+ }
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GridSnapLayoutRefiner = void 0;
4
+ class GridSnapLayoutRefiner {
5
+ constructor(opts = {}) {
6
+ this.opts = opts;
7
+ this.id = 'grid-snap';
8
+ }
9
+ refine(entities, _ctx) {
10
+ if (entities.length === 0)
11
+ return entities;
12
+ const out = entities.map(cloneEntity);
13
+ if (this.opts.align !== false) {
14
+ const tol = this.opts.alignTolerance ?? autoTolerance(out);
15
+ const xs = clusterSnap(out.map(e => e.position.x), tol);
16
+ const ys = clusterSnap(out.map(e => e.position.y), tol);
17
+ out.forEach((e, i) => moveTo(e, xs[i], ys[i]));
18
+ }
19
+ const g = this.opts.grid;
20
+ if (g && g > 0) {
21
+ out.forEach(e => moveTo(e, Math.round(e.position.x / g) * g, Math.round(e.position.y / g) * g));
22
+ }
23
+ return out;
24
+ }
25
+ }
26
+ exports.GridSnapLayoutRefiner = GridSnapLayoutRefiner;
27
+ /** 위치를 (nx,ny)로 옮기고 bbox 도 같은 delta 로 평행이동 (size 보존). */
28
+ function moveTo(e, nx, ny) {
29
+ const dx = nx - e.position.x;
30
+ const dy = ny - e.position.y;
31
+ if (dx === 0 && dy === 0)
32
+ return;
33
+ e.position = { x: nx, y: ny };
34
+ e.bbox = {
35
+ min: { x: e.bbox.min.x + dx, y: e.bbox.min.y + dy },
36
+ max: { x: e.bbox.max.x + dx, y: e.bbox.max.y + dy }
37
+ };
38
+ }
39
+ /** 단일연결 클러스터링 — 정렬 후 인접 간격 ≤ tol 이면 한 클러스터, 클러스터 평균으로 스냅. */
40
+ function clusterSnap(vals, tol) {
41
+ const order = vals.map((v, i) => ({ v, i })).sort((a, b) => a.v - b.v);
42
+ const snapped = new Array(vals.length);
43
+ const flush = (s, e) => {
44
+ let sum = 0;
45
+ for (let k = s; k < e; k++)
46
+ sum += order[k].v;
47
+ const mean = sum / (e - s);
48
+ for (let k = s; k < e; k++)
49
+ snapped[order[k].i] = mean;
50
+ };
51
+ let start = 0;
52
+ for (let k = 1; k < order.length; k++) {
53
+ if (order[k].v - order[k - 1].v > tol) {
54
+ flush(start, k);
55
+ start = k;
56
+ }
57
+ }
58
+ flush(start, order.length);
59
+ return snapped;
60
+ }
61
+ /** 자동 허용오차 — 엔티티 크기 중앙값의 40%. 크기 없으면 좌표 spread 의 2%. */
62
+ function autoTolerance(entities) {
63
+ const dims = [];
64
+ for (const e of entities)
65
+ if (e.size)
66
+ dims.push(e.size.width, e.size.height);
67
+ if (dims.length)
68
+ return 0.4 * median(dims);
69
+ const xs = entities.map(e => e.position.x);
70
+ const ys = entities.map(e => e.position.y);
71
+ const spread = Math.max(Math.max(...xs) - Math.min(...xs), Math.max(...ys) - Math.min(...ys));
72
+ return 0.02 * (spread || 1);
73
+ }
74
+ function median(arr) {
75
+ const s = [...arr].sort((a, b) => a - b);
76
+ const m = Math.floor(s.length / 2);
77
+ return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
78
+ }
79
+ function cloneEntity(e) {
80
+ return {
81
+ ...e,
82
+ position: { ...e.position },
83
+ size: e.size ? { ...e.size } : undefined,
84
+ bbox: { min: { ...e.bbox.min }, max: { ...e.bbox.max } },
85
+ attribs: { ...e.attribs }
86
+ };
87
+ }
88
+ //# sourceMappingURL=layout-refiner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout-refiner.js","sourceRoot":"","sources":["../../../server/service/extraction/layout-refiner.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * GridSnapLayoutRefiner — 추출된 엔티티 레이아웃을 *정리*. Phase Board-AI §3.8 의 LayoutRefiner.\n * 모든 경로(벡터·CV·VLM) 공용 품질 레버.\n *\n * - align : 근접한 left/top 좌표를 단일연결 클러스터로 묶어 평균으로 스냅 → 행/열 정렬.\n * - grid : (옵션) 위치를 grid step 으로 스냅.\n *\n * 순수·결정적 (같은 입력 → 같은 출력). 크기는 보존, 위치 이동분만큼 bbox 평행이동.\n */\nimport type { UniversalEntity } from '../types/index'\nimport type { LayoutRefiner, ExtractionContext } from './contracts'\n\nexport interface LayoutRefineOptions {\n /** 근접 좌표 정렬 (default true). */\n align?: boolean\n /** 정렬 클러스터 허용오차. 미지정 시 엔티티 크기 중앙값으로 자동. */\n alignTolerance?: number\n /** >0 이면 위치를 이 step 으로 스냅. */\n grid?: number\n}\n\nexport class GridSnapLayoutRefiner implements LayoutRefiner {\n readonly id = 'grid-snap'\n\n constructor(private readonly opts: LayoutRefineOptions = {}) {}\n\n refine(entities: UniversalEntity[], _ctx?: ExtractionContext): UniversalEntity[] {\n if (entities.length === 0) return entities\n const out = entities.map(cloneEntity)\n\n if (this.opts.align !== false) {\n const tol = this.opts.alignTolerance ?? autoTolerance(out)\n const xs = clusterSnap(out.map(e => e.position.x), tol)\n const ys = clusterSnap(out.map(e => e.position.y), tol)\n out.forEach((e, i) => moveTo(e, xs[i], ys[i]))\n }\n\n const g = this.opts.grid\n if (g && g > 0) {\n out.forEach(e => moveTo(e, Math.round(e.position.x / g) * g, Math.round(e.position.y / g) * g))\n }\n return out\n }\n}\n\n/** 위치를 (nx,ny)로 옮기고 bbox 도 같은 delta 로 평행이동 (size 보존). */\nfunction moveTo(e: UniversalEntity, nx: number, ny: number): void {\n const dx = nx - e.position.x\n const dy = ny - e.position.y\n if (dx === 0 && dy === 0) return\n e.position = { x: nx, y: ny }\n e.bbox = {\n min: { x: e.bbox.min.x + dx, y: e.bbox.min.y + dy },\n max: { x: e.bbox.max.x + dx, y: e.bbox.max.y + dy }\n }\n}\n\n/** 단일연결 클러스터링 — 정렬 후 인접 간격 ≤ tol 이면 한 클러스터, 클러스터 평균으로 스냅. */\nfunction clusterSnap(vals: number[], tol: number): number[] {\n const order = vals.map((v, i) => ({ v, i })).sort((a, b) => a.v - b.v)\n const snapped = new Array<number>(vals.length)\n const flush = (s: number, e: number) => {\n let sum = 0\n for (let k = s; k < e; k++) sum += order[k].v\n const mean = sum / (e - s)\n for (let k = s; k < e; k++) snapped[order[k].i] = mean\n }\n let start = 0\n for (let k = 1; k < order.length; k++) {\n if (order[k].v - order[k - 1].v > tol) {\n flush(start, k)\n start = k\n }\n }\n flush(start, order.length)\n return snapped\n}\n\n/** 자동 허용오차 — 엔티티 크기 중앙값의 40%. 크기 없으면 좌표 spread 의 2%. */\nfunction autoTolerance(entities: UniversalEntity[]): number {\n const dims: number[] = []\n for (const e of entities) if (e.size) dims.push(e.size.width, e.size.height)\n if (dims.length) return 0.4 * median(dims)\n const xs = entities.map(e => e.position.x)\n const ys = entities.map(e => e.position.y)\n const spread = Math.max(Math.max(...xs) - Math.min(...xs), Math.max(...ys) - Math.min(...ys))\n return 0.02 * (spread || 1)\n}\n\nfunction median(arr: number[]): number {\n const s = [...arr].sort((a, b) => a - b)\n const m = Math.floor(s.length / 2)\n return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2\n}\n\nfunction cloneEntity(e: UniversalEntity): UniversalEntity {\n return {\n ...e,\n position: { ...e.position },\n size: e.size ? { ...e.size } : undefined,\n bbox: { min: { ...e.bbox.min }, max: { ...e.bbox.max } },\n attribs: { ...e.attribs }\n }\n}\n"]}
@@ -0,0 +1,11 @@
1
+ import type { UniversalEntity, BoundingBox, AdapterInput } from '../types/index';
2
+ import type { SourceKind, VariantDraft, ExtractionContext } from './contracts';
3
+ export interface HybridExtractResult {
4
+ /** default 시안 entities (= variants[0]). */
5
+ entities: UniversalEntity[];
6
+ /** 다중 시안 (선택 UX). */
7
+ variants: VariantDraft[];
8
+ bbox: BoundingBox;
9
+ sourceKind: SourceKind;
10
+ }
11
+ export declare function extractHybrid(input: AdapterInput, ctx?: ExtractionContext, filename?: string): Promise<HybridExtractResult>;
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractHybrid = extractHybrid;
4
+ const source_router_1 = require("./source-router");
5
+ async function extractHybrid(input, ctx = {}, filename) {
6
+ const kind = await (0, source_router_1.detectSourceKind)(input, filename);
7
+ const set = (0, source_router_1.getExtractorSet)(kind);
8
+ if (!set) {
9
+ throw new Error(`[board-import/extraction] '${kind}' ExtractorSet 미등록 — registerExtractorSet 로 주입 필요`);
10
+ }
11
+ // 1. 기하(위치) + 2. 라벨(식별/스케일 단서) — 공유 추출.
12
+ const primitives = await set.geometry.extract(input, ctx);
13
+ const labels = set.labels ? await set.labels.extract(input, ctx) : [];
14
+ // 3. 시안 생성 — 전략 있으면 N안, 없으면 단일 default.
15
+ let drafts = set.variants
16
+ ? await set.variants.interpret(primitives, labels, set, ctx)
17
+ : [{ id: 'default', label: 'default', entities: await fuse(primitives, labels, set, ctx) }];
18
+ if (drafts.length === 0)
19
+ drafts = [{ id: 'default', label: 'default', entities: [] }];
20
+ // 4. 시안별 스케일 보정 + 5. 레이아웃 정리.
21
+ for (const d of drafts) {
22
+ if (set.scale)
23
+ applyScale(d.entities, await set.scale.calibrate(labels, primitives, ctx));
24
+ if (set.refiner)
25
+ d.entities = set.refiner.refine(d.entities, ctx);
26
+ }
27
+ return {
28
+ entities: drafts[0].entities,
29
+ variants: drafts,
30
+ bbox: outerBbox(drafts[0].entities),
31
+ sourceKind: kind
32
+ };
33
+ }
34
+ // ── 기본 융합 (스캐폴딩) ────────────────────────────────────────────────────────
35
+ // 기하 프리미티브 → UniversalEntity, 가장 가까운 라벨을 name 으로 앵커.
36
+ // TODO(본격): region 별 set.recognizer.recognize → attribs.semanticType/componentType + categoryHint.
37
+ async function fuse(primitives, labels, set, ctx) {
38
+ return Promise.all(primitives.map(async (p, i) => {
39
+ const lbl = nearestLabel(p, labels);
40
+ if (p.entity) {
41
+ // 벡터: 이미 완전한 엔티티(타입/속성 보유) — 무손실 사용. name 없으면 근접 라벨로 보강.
42
+ const e = p.entity;
43
+ if (!e.attribs.name && lbl)
44
+ e.attribs = { ...e.attribs, name: lbl.text };
45
+ return e;
46
+ }
47
+ const e = primitiveToEntity(p, i);
48
+ if (lbl)
49
+ e.attribs.name = lbl.text;
50
+ // CV 등 bare 프리미티브 — Recognizer 로 의미(componentType) 부여. (좌표 아님)
51
+ if (set.recognizer) {
52
+ const region = lbl ? { ...p, meta: { ...p.meta, label: lbl.text } } : p;
53
+ const r = await set.recognizer.recognize(region, ctx);
54
+ if (r.componentType) {
55
+ e.attribs.semanticType = r.componentType;
56
+ e.attribs.confidence = r.confidence;
57
+ }
58
+ if (r.attribs)
59
+ Object.assign(e.attribs, r.attribs);
60
+ }
61
+ return e;
62
+ }));
63
+ }
64
+ function primitiveToEntity(p, i) {
65
+ const w = p.bbox.max.x - p.bbox.min.x;
66
+ const h = p.bbox.max.y - p.bbox.min.y;
67
+ return {
68
+ uid: `ext-${i}`,
69
+ kind: 'shape',
70
+ position: { x: p.bbox.min.x, y: p.bbox.min.y },
71
+ size: { width: w, height: h },
72
+ bbox: p.bbox,
73
+ attribs: {}
74
+ };
75
+ }
76
+ function center(b) {
77
+ return { x: (b.min.x + b.max.x) / 2, y: (b.min.y + b.max.y) / 2 };
78
+ }
79
+ function nearestLabel(p, labels) {
80
+ if (!labels.length)
81
+ return undefined;
82
+ const c = center(p.bbox);
83
+ let best;
84
+ let bestD = Infinity;
85
+ for (const l of labels) {
86
+ const lc = center(l.bbox);
87
+ const d = (lc.x - c.x) ** 2 + (lc.y - c.y) ** 2;
88
+ if (d < bestD) {
89
+ bestD = d;
90
+ best = l;
91
+ }
92
+ }
93
+ return best;
94
+ }
95
+ function applyScale(entities, s) {
96
+ const f = s.unitsPerPixel;
97
+ const ox = s.origin.x;
98
+ const oy = s.origin.y;
99
+ for (const e of entities) {
100
+ e.position = { x: (e.position.x - ox) * f, y: (e.position.y - oy) * f };
101
+ if (e.size)
102
+ e.size = { width: e.size.width * f, height: e.size.height * f };
103
+ e.bbox = {
104
+ min: { x: (e.bbox.min.x - ox) * f, y: (e.bbox.min.y - oy) * f },
105
+ max: { x: (e.bbox.max.x - ox) * f, y: (e.bbox.max.y - oy) * f }
106
+ };
107
+ }
108
+ }
109
+ function outerBbox(entities) {
110
+ if (!entities.length)
111
+ return { min: { x: 0, y: 0 }, max: { x: 0, y: 0 } };
112
+ let minX = Infinity;
113
+ let minY = Infinity;
114
+ let maxX = -Infinity;
115
+ let maxY = -Infinity;
116
+ for (const e of entities) {
117
+ minX = Math.min(minX, e.bbox.min.x);
118
+ minY = Math.min(minY, e.bbox.min.y);
119
+ maxX = Math.max(maxX, e.bbox.max.x);
120
+ maxY = Math.max(maxY, e.bbox.max.y);
121
+ }
122
+ return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY } };
123
+ }
124
+ //# sourceMappingURL=pipeline.js.map