@things-factory/board-import 10.0.0-beta.108
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/config/config.development.js +44 -0
- package/config/config.production.js +18 -0
- package/dist-server/index.d.ts +7 -0
- package/dist-server/index.js +11 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/service/adapters/dxf-adapter.d.ts +8 -0
- package/dist-server/service/adapters/dxf-adapter.js +220 -0
- package/dist-server/service/adapters/dxf-adapter.js.map +1 -0
- package/dist-server/service/adapters/image-adapter.d.ts +32 -0
- package/dist-server/service/adapters/image-adapter.js +420 -0
- package/dist-server/service/adapters/image-adapter.js.map +1 -0
- package/dist-server/service/ai/types.d.ts +185 -0
- package/dist-server/service/ai/types.js +378 -0
- package/dist-server/service/ai/types.js.map +1 -0
- package/dist-server/service/converters/generic-to.d.ts +31 -0
- package/dist-server/service/converters/generic-to.js +84 -0
- package/dist-server/service/converters/generic-to.js.map +1 -0
- package/dist-server/service/extraction/contracts.d.ts +112 -0
- package/dist-server/service/extraction/contracts.js +3 -0
- package/dist-server/service/extraction/contracts.js.map +1 -0
- package/dist-server/service/extraction/index.d.ts +17 -0
- package/dist-server/service/extraction/index.js +62 -0
- package/dist-server/service/extraction/index.js.map +1 -0
- package/dist-server/service/extraction/label-scale-calibrator.d.ts +5 -0
- package/dist-server/service/extraction/label-scale-calibrator.js +111 -0
- package/dist-server/service/extraction/label-scale-calibrator.js.map +1 -0
- package/dist-server/service/extraction/layout-refiner.d.ts +16 -0
- package/dist-server/service/extraction/layout-refiner.js +88 -0
- package/dist-server/service/extraction/layout-refiner.js.map +1 -0
- package/dist-server/service/extraction/pipeline.d.ts +11 -0
- package/dist-server/service/extraction/pipeline.js +124 -0
- package/dist-server/service/extraction/pipeline.js.map +1 -0
- package/dist-server/service/extraction/source-router.d.ts +17 -0
- package/dist-server/service/extraction/source-router.js +40 -0
- package/dist-server/service/extraction/source-router.js.map +1 -0
- package/dist-server/service/extraction/stubs.d.ts +18 -0
- package/dist-server/service/extraction/stubs.js +38 -0
- package/dist-server/service/extraction/stubs.js.map +1 -0
- package/dist-server/service/extraction/vector-geometry.d.ts +7 -0
- package/dist-server/service/extraction/vector-geometry.js +21 -0
- package/dist-server/service/extraction/vector-geometry.js.map +1 -0
- package/dist-server/service/extraction/vlm-recognizer.d.ts +18 -0
- package/dist-server/service/extraction/vlm-recognizer.js +78 -0
- package/dist-server/service/extraction/vlm-recognizer.js.map +1 -0
- package/dist-server/service/import-session/import-actions.d.ts +79 -0
- package/dist-server/service/import-session/import-actions.js +98 -0
- package/dist-server/service/import-session/import-actions.js.map +1 -0
- package/dist-server/service/import-session/import-session-resolver.d.ts +39 -0
- package/dist-server/service/import-session/import-session-resolver.js +263 -0
- package/dist-server/service/import-session/import-session-resolver.js.map +1 -0
- package/dist-server/service/import-session/import-session.d.ts +28 -0
- package/dist-server/service/import-session/import-session.js +152 -0
- package/dist-server/service/import-session/import-session.js.map +1 -0
- package/dist-server/service/import-session/import-worker.d.ts +1 -0
- package/dist-server/service/import-session/import-worker.js +121 -0
- package/dist-server/service/import-session/import-worker.js.map +1 -0
- package/dist-server/service/import-session/index.d.ts +8 -0
- package/dist-server/service/import-session/index.js +19 -0
- package/dist-server/service/import-session/index.js.map +1 -0
- package/dist-server/service/import-session/materialize-from-session.d.ts +66 -0
- package/dist-server/service/import-session/materialize-from-session.js +104 -0
- package/dist-server/service/import-session/materialize-from-session.js.map +1 -0
- package/dist-server/service/import-session/suggest-board-name.d.ts +80 -0
- package/dist-server/service/import-session/suggest-board-name.js +195 -0
- package/dist-server/service/import-session/suggest-board-name.js.map +1 -0
- package/dist-server/service/import-tools.d.ts +10 -0
- package/dist-server/service/import-tools.js +227 -0
- package/dist-server/service/import-tools.js.map +1 -0
- package/dist-server/service/index.d.ts +26 -0
- package/dist-server/service/index.js +60 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/pipeline/index.d.ts +66 -0
- package/dist-server/service/pipeline/index.js +127 -0
- package/dist-server/service/pipeline/index.js.map +1 -0
- package/dist-server/service/pipeline/stage2-mapping.d.ts +29 -0
- package/dist-server/service/pipeline/stage2-mapping.js +231 -0
- package/dist-server/service/pipeline/stage2-mapping.js.map +1 -0
- package/dist-server/service/pipeline/stage3-board.d.ts +52 -0
- package/dist-server/service/pipeline/stage3-board.js +108 -0
- package/dist-server/service/pipeline/stage3-board.js.map +1 -0
- package/dist-server/service/pipeline/stage4-binding.d.ts +9 -0
- package/dist-server/service/pipeline/stage4-binding.js +28 -0
- package/dist-server/service/pipeline/stage4-binding.js.map +1 -0
- package/dist-server/service/registry/index.d.ts +27 -0
- package/dist-server/service/registry/index.js +108 -0
- package/dist-server/service/registry/index.js.map +1 -0
- package/dist-server/service/types/index.d.ts +152 -0
- package/dist-server/service/types/index.js +3 -0
- package/dist-server/service/types/index.js.map +1 -0
- package/dist-server/service/types/registry.d.ts +175 -0
- package/dist-server/service/types/registry.js +3 -0
- package/dist-server/service/types/registry.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +35 -0
- package/probe/gcp-vision-probe.mjs +114 -0
- package/probe/opencv-geometry-probe.py +98 -0
- package/server/index.ts +7 -0
- package/server/service/adapters/dxf-adapter.ts +230 -0
- package/server/service/adapters/image-adapter.test.ts +545 -0
- package/server/service/adapters/image-adapter.ts +464 -0
- package/server/service/ai/types.ts +619 -0
- package/server/service/converters/generic-to.test.ts +105 -0
- package/server/service/converters/generic-to.ts +121 -0
- package/server/service/extraction/contracts.ts +142 -0
- package/server/service/extraction/extraction.test.ts +374 -0
- package/server/service/extraction/index.ts +51 -0
- package/server/service/extraction/label-scale-calibrator.ts +124 -0
- package/server/service/extraction/layout-refiner.ts +104 -0
- package/server/service/extraction/pipeline.ts +163 -0
- package/server/service/extraction/source-router.ts +45 -0
- package/server/service/extraction/stubs.ts +46 -0
- package/server/service/extraction/vector-geometry.ts +26 -0
- package/server/service/extraction/vlm-recognizer.ts +87 -0
- package/server/service/import-session/import-actions.test.ts +185 -0
- package/server/service/import-session/import-actions.ts +164 -0
- package/server/service/import-session/import-session-resolver.ts +245 -0
- package/server/service/import-session/import-session.ts +141 -0
- package/server/service/import-session/import-worker.ts +132 -0
- package/server/service/import-session/index.ts +24 -0
- package/server/service/import-session/materialize-from-session.test.ts +274 -0
- package/server/service/import-session/materialize-from-session.ts +158 -0
- package/server/service/import-session/suggest-board-name.test.ts +271 -0
- package/server/service/import-session/suggest-board-name.ts +279 -0
- package/server/service/import-tools.test.ts +137 -0
- package/server/service/import-tools.ts +255 -0
- package/server/service/index.ts +117 -0
- package/server/service/pipeline/index.ts +184 -0
- package/server/service/pipeline/stage2-mapping.test.ts +204 -0
- package/server/service/pipeline/stage2-mapping.ts +270 -0
- package/server/service/pipeline/stage3-board.test.ts +133 -0
- package/server/service/pipeline/stage3-board.ts +179 -0
- package/server/service/pipeline/stage4-binding.ts +31 -0
- package/server/service/registry/index.ts +127 -0
- package/server/service/types/index.ts +169 -0
- package/server/service/types/registry.ts +186 -0
- package/things-factory.config.js +1 -0
- package/tsconfig.json +9 -0
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../../../server/service/extraction/pipeline.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 하이브리드 추출 파이프라인 (골격) — Phase Board-AI §3.8.\n *\n * detect → geometry + labels → [VariantStrategy 또는 단일 fuse] → 시안별 scale → refine\n *\n * 기하는 *공유* (정확) → 시안들은 해석만 다름 → 3안 선택 UX 보존하되 셋 다 레이아웃 정상.\n * impl 은 ExtractorSet 으로 주입 (스캐폴딩엔 stub). 기존 어댑터 경로와 독립.\n */\nimport type { UniversalEntity, BoundingBox, AdapterInput } from '../types/index'\nimport type {\n SourceKind,\n ExtractorSet,\n GeometricPrimitive,\n Label,\n VariantDraft,\n ScaleResult,\n ExtractionContext\n} from './contracts'\nimport { detectSourceKind, getExtractorSet } from './source-router'\n\nexport interface HybridExtractResult {\n /** default 시안 entities (= variants[0]). */\n entities: UniversalEntity[]\n /** 다중 시안 (선택 UX). */\n variants: VariantDraft[]\n bbox: BoundingBox\n sourceKind: SourceKind\n}\n\nexport async function extractHybrid(\n input: AdapterInput,\n ctx: ExtractionContext = {},\n filename?: string\n): Promise<HybridExtractResult> {\n const kind = await detectSourceKind(input, filename)\n const set = getExtractorSet(kind)\n if (!set) {\n throw new Error(`[board-import/extraction] '${kind}' ExtractorSet 미등록 — registerExtractorSet 로 주입 필요`)\n }\n\n // 1. 기하(위치) + 2. 라벨(식별/스케일 단서) — 공유 추출.\n const primitives = await set.geometry.extract(input, ctx)\n const labels: Label[] = set.labels ? await set.labels.extract(input, ctx) : []\n\n // 3. 시안 생성 — 전략 있으면 N안, 없으면 단일 default.\n let drafts: VariantDraft[] = set.variants\n ? await set.variants.interpret(primitives, labels, set, ctx)\n : [{ id: 'default', label: 'default', entities: await fuse(primitives, labels, set, ctx) }]\n if (drafts.length === 0) drafts = [{ id: 'default', label: 'default', entities: [] }]\n\n // 4. 시안별 스케일 보정 + 5. 레이아웃 정리.\n for (const d of drafts) {\n if (set.scale) applyScale(d.entities, await set.scale.calibrate(labels, primitives, ctx))\n if (set.refiner) d.entities = set.refiner.refine(d.entities, ctx)\n }\n\n return {\n entities: drafts[0].entities,\n variants: drafts,\n bbox: outerBbox(drafts[0].entities),\n sourceKind: kind\n }\n}\n\n// ── 기본 융합 (스캐폴딩) ────────────────────────────────────────────────────────\n// 기하 프리미티브 → UniversalEntity, 가장 가까운 라벨을 name 으로 앵커.\n// TODO(본격): region 별 set.recognizer.recognize → attribs.semanticType/componentType + categoryHint.\nasync function fuse(\n primitives: GeometricPrimitive[],\n labels: Label[],\n set: ExtractorSet,\n ctx?: ExtractionContext\n): Promise<UniversalEntity[]> {\n return Promise.all(\n primitives.map(async (p, i) => {\n const lbl = nearestLabel(p, labels)\n\n if (p.entity) {\n // 벡터: 이미 완전한 엔티티(타입/속성 보유) — 무손실 사용. name 없으면 근접 라벨로 보강.\n const e = p.entity\n if (!e.attribs.name && lbl) e.attribs = { ...e.attribs, name: lbl.text }\n return e\n }\n\n const e = primitiveToEntity(p, i)\n if (lbl) e.attribs.name = lbl.text\n\n // CV 등 bare 프리미티브 — Recognizer 로 의미(componentType) 부여. (좌표 아님)\n if (set.recognizer) {\n const region = lbl ? { ...p, meta: { ...p.meta, label: lbl.text } } : p\n const r = await set.recognizer.recognize(region, ctx)\n if (r.componentType) {\n e.attribs.semanticType = r.componentType\n e.attribs.confidence = r.confidence\n }\n if (r.attribs) Object.assign(e.attribs, r.attribs)\n }\n return e\n })\n )\n}\n\nfunction primitiveToEntity(p: GeometricPrimitive, i: number): UniversalEntity {\n const w = p.bbox.max.x - p.bbox.min.x\n const h = p.bbox.max.y - p.bbox.min.y\n return {\n uid: `ext-${i}`,\n kind: 'shape',\n position: { x: p.bbox.min.x, y: p.bbox.min.y },\n size: { width: w, height: h },\n bbox: p.bbox,\n attribs: {}\n }\n}\n\nfunction center(b: BoundingBox) {\n return { x: (b.min.x + b.max.x) / 2, y: (b.min.y + b.max.y) / 2 }\n}\n\nfunction nearestLabel(p: GeometricPrimitive, labels: Label[]): Label | undefined {\n if (!labels.length) return undefined\n const c = center(p.bbox)\n let best: Label | undefined\n let bestD = Infinity\n for (const l of labels) {\n const lc = center(l.bbox)\n const d = (lc.x - c.x) ** 2 + (lc.y - c.y) ** 2\n if (d < bestD) {\n bestD = d\n best = l\n }\n }\n return best\n}\n\nfunction applyScale(entities: UniversalEntity[], s: ScaleResult): void {\n const f = s.unitsPerPixel\n const ox = s.origin.x\n const oy = s.origin.y\n for (const e of entities) {\n e.position = { x: (e.position.x - ox) * f, y: (e.position.y - oy) * f }\n if (e.size) e.size = { width: e.size.width * f, height: e.size.height * f }\n e.bbox = {\n min: { x: (e.bbox.min.x - ox) * f, y: (e.bbox.min.y - oy) * f },\n max: { x: (e.bbox.max.x - ox) * f, y: (e.bbox.max.y - oy) * f }\n }\n }\n}\n\nfunction outerBbox(entities: UniversalEntity[]): BoundingBox {\n if (!entities.length) return { min: { x: 0, y: 0 }, max: { x: 0, y: 0 } }\n let minX = Infinity\n let minY = Infinity\n let maxX = -Infinity\n let maxY = -Infinity\n for (const e of entities) {\n minX = Math.min(minX, e.bbox.min.x)\n minY = Math.min(minY, e.bbox.min.y)\n maxX = Math.max(maxX, e.bbox.max.x)\n maxY = Math.max(maxY, e.bbox.max.y)\n }\n return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY } }\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AdapterInput } from '../types/index';
|
|
2
|
+
import type { SourceKind, ExtractorSet } from './contracts';
|
|
3
|
+
/**
|
|
4
|
+
* 입력 → 소스 분류. 현재는 확장자 기반의 1차 분류.
|
|
5
|
+
* - pdf: vector 우선 가정 (실제론 vector/raster 페이지 판별 필요 — 추후).
|
|
6
|
+
* - 비트맵: 일단 raster-topdown. top-down vs photo 세분은 VLM 1-pass 분류로 (추후).
|
|
7
|
+
*/
|
|
8
|
+
export declare function detectSourceKind(_input: AdapterInput, filename?: string): Promise<SourceKind>;
|
|
9
|
+
export type ExtractorSetFactory = () => ExtractorSet;
|
|
10
|
+
/** 소스별 추출기 묶음 등록 (impl 주입 지점). */
|
|
11
|
+
export declare function registerExtractorSet(kind: SourceKind, factory: ExtractorSetFactory): void;
|
|
12
|
+
/** 소스별 추출기 묶음 조회. 미등록 시 undefined. */
|
|
13
|
+
export declare function getExtractorSet(kind: SourceKind): ExtractorSet | undefined;
|
|
14
|
+
/** 등록된 소스 종류 (진단/검사용). */
|
|
15
|
+
export declare function registeredSourceKinds(): SourceKind[];
|
|
16
|
+
/** 등록 초기화 (테스트 격리 / 재구성용). */
|
|
17
|
+
export declare function clearExtractorSets(): void;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectSourceKind = detectSourceKind;
|
|
4
|
+
exports.registerExtractorSet = registerExtractorSet;
|
|
5
|
+
exports.getExtractorSet = getExtractorSet;
|
|
6
|
+
exports.registeredSourceKinds = registeredSourceKinds;
|
|
7
|
+
exports.clearExtractorSets = clearExtractorSets;
|
|
8
|
+
const VECTOR_EXT = new Set(['dxf', 'svg', 'ifc']);
|
|
9
|
+
const RASTER_EXT = new Set(['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp']);
|
|
10
|
+
/**
|
|
11
|
+
* 입력 → 소스 분류. 현재는 확장자 기반의 1차 분류.
|
|
12
|
+
* - pdf: vector 우선 가정 (실제론 vector/raster 페이지 판별 필요 — 추후).
|
|
13
|
+
* - 비트맵: 일단 raster-topdown. top-down vs photo 세분은 VLM 1-pass 분류로 (추후).
|
|
14
|
+
*/
|
|
15
|
+
async function detectSourceKind(_input, filename) {
|
|
16
|
+
const ext = (filename?.split('.').pop() || '').toLowerCase();
|
|
17
|
+
if (VECTOR_EXT.has(ext) || ext === 'pdf')
|
|
18
|
+
return 'vector';
|
|
19
|
+
if (RASTER_EXT.has(ext))
|
|
20
|
+
return 'raster-topdown';
|
|
21
|
+
return 'unknown';
|
|
22
|
+
}
|
|
23
|
+
const _registry = new Map();
|
|
24
|
+
/** 소스별 추출기 묶음 등록 (impl 주입 지점). */
|
|
25
|
+
function registerExtractorSet(kind, factory) {
|
|
26
|
+
_registry.set(kind, factory);
|
|
27
|
+
}
|
|
28
|
+
/** 소스별 추출기 묶음 조회. 미등록 시 undefined. */
|
|
29
|
+
function getExtractorSet(kind) {
|
|
30
|
+
return _registry.get(kind)?.();
|
|
31
|
+
}
|
|
32
|
+
/** 등록된 소스 종류 (진단/검사용). */
|
|
33
|
+
function registeredSourceKinds() {
|
|
34
|
+
return [..._registry.keys()];
|
|
35
|
+
}
|
|
36
|
+
/** 등록 초기화 (테스트 격리 / 재구성용). */
|
|
37
|
+
function clearExtractorSets() {
|
|
38
|
+
_registry.clear();
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=source-router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source-router.js","sourceRoot":"","sources":["../../../server/service/extraction/source-router.ts"],"names":[],"mappings":";;AAeA,4CAKC;AAOD,oDAEC;AAGD,0CAEC;AAGD,sDAEC;AAGD,gDAEC;AArCD,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAA;AACjD,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAA;AAExE;;;;GAIG;AACI,KAAK,UAAU,gBAAgB,CAAC,MAAoB,EAAE,QAAiB;IAC5E,MAAM,GAAG,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;IAC5D,IAAI,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,KAAK,KAAK;QAAE,OAAO,QAAQ,CAAA;IACzD,IAAI,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO,gBAAgB,CAAA;IAChD,OAAO,SAAS,CAAA;AAClB,CAAC;AAID,MAAM,SAAS,GAAG,IAAI,GAAG,EAAmC,CAAA;AAE5D,kCAAkC;AAClC,SAAgB,oBAAoB,CAAC,IAAgB,EAAE,OAA4B;IACjF,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;AAC9B,CAAC;AAED,sCAAsC;AACtC,SAAgB,eAAe,CAAC,IAAgB;IAC9C,OAAO,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,CAAA;AAChC,CAAC;AAED,0BAA0B;AAC1B,SAAgB,qBAAqB;IACnC,OAAO,CAAC,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC,CAAA;AAC9B,CAAC;AAED,8BAA8B;AAC9B,SAAgB,kBAAkB;IAChC,SAAS,CAAC,KAAK,EAAE,CAAA;AACnB,CAAC","sourcesContent":["/*\n * 소스 라우팅 — 입력(+파일명)을 SourceKind 로 분류하고, 그 경로의 ExtractorSet 을 돌려준다.\n * impl 묶음은 register 로 주입 (스캐폴딩 단계엔 stub 묶음). 기존 어댑터 경로는 무관/보존.\n */\nimport type { AdapterInput } from '../types/index'\nimport type { SourceKind, ExtractorSet } from './contracts'\n\nconst VECTOR_EXT = new Set(['dxf', 'svg', 'ifc'])\nconst RASTER_EXT = new Set(['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp'])\n\n/**\n * 입력 → 소스 분류. 현재는 확장자 기반의 1차 분류.\n * - pdf: vector 우선 가정 (실제론 vector/raster 페이지 판별 필요 — 추후).\n * - 비트맵: 일단 raster-topdown. top-down vs photo 세분은 VLM 1-pass 분류로 (추후).\n */\nexport async function detectSourceKind(_input: AdapterInput, filename?: string): Promise<SourceKind> {\n const ext = (filename?.split('.').pop() || '').toLowerCase()\n if (VECTOR_EXT.has(ext) || ext === 'pdf') return 'vector'\n if (RASTER_EXT.has(ext)) return 'raster-topdown'\n return 'unknown'\n}\n\nexport type ExtractorSetFactory = () => ExtractorSet\n\nconst _registry = new Map<SourceKind, ExtractorSetFactory>()\n\n/** 소스별 추출기 묶음 등록 (impl 주입 지점). */\nexport function registerExtractorSet(kind: SourceKind, factory: ExtractorSetFactory): void {\n _registry.set(kind, factory)\n}\n\n/** 소스별 추출기 묶음 조회. 미등록 시 undefined. */\nexport function getExtractorSet(kind: SourceKind): ExtractorSet | undefined {\n return _registry.get(kind)?.()\n}\n\n/** 등록된 소스 종류 (진단/검사용). */\nexport function registeredSourceKinds(): SourceKind[] {\n return [..._registry.keys()]\n}\n\n/** 등록 초기화 (테스트 격리 / 재구성용). */\nexport function clearExtractorSets(): void {\n _registry.clear()\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { UniversalEntity } from '../types/index';
|
|
2
|
+
import type { GeometryExtractor, LabelExtractor, LayoutRefiner, GeometricPrimitive, Label, ExtractionContext } from './contracts';
|
|
3
|
+
export declare class CvGeometryExtractor implements GeometryExtractor {
|
|
4
|
+
readonly id = "cv-geometry";
|
|
5
|
+
extract(): Promise<GeometricPrimitive[]>;
|
|
6
|
+
}
|
|
7
|
+
export declare class CloudOcrLabelExtractor implements LabelExtractor {
|
|
8
|
+
readonly id = "cloud-ocr";
|
|
9
|
+
extract(): Promise<Label[]>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 레이아웃 정리 stub — 정렬/스냅/겹침해소는 미구현이라 *그대로 통과*(no-op).
|
|
13
|
+
* (throw 가 아니라 안전한 항등 — refiner 미구현이어도 파이프라인이 깨지지 않도록.)
|
|
14
|
+
*/
|
|
15
|
+
export declare class IdentityLayoutRefiner implements LayoutRefiner {
|
|
16
|
+
readonly id = "identity";
|
|
17
|
+
refine(entities: UniversalEntity[], _ctx?: ExtractionContext): UniversalEntity[];
|
|
18
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IdentityLayoutRefiner = exports.CloudOcrLabelExtractor = exports.CvGeometryExtractor = void 0;
|
|
4
|
+
const NI = (what) => {
|
|
5
|
+
throw new Error(`[board-import/extraction] ${what} 미구현 (스캐폴딩) — Phase B 본격 테스트 단계에서 구현`);
|
|
6
|
+
};
|
|
7
|
+
class CvGeometryExtractor {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.id = 'cv-geometry';
|
|
10
|
+
}
|
|
11
|
+
async extract() {
|
|
12
|
+
return NI('CvGeometryExtractor.extract (OpenCV 선/사각/연결성분)');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.CvGeometryExtractor = CvGeometryExtractor;
|
|
16
|
+
class CloudOcrLabelExtractor {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.id = 'cloud-ocr';
|
|
19
|
+
}
|
|
20
|
+
async extract() {
|
|
21
|
+
return NI('CloudOcrLabelExtractor.extract (GCP Vision / Textract)');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.CloudOcrLabelExtractor = CloudOcrLabelExtractor;
|
|
25
|
+
/**
|
|
26
|
+
* 레이아웃 정리 stub — 정렬/스냅/겹침해소는 미구현이라 *그대로 통과*(no-op).
|
|
27
|
+
* (throw 가 아니라 안전한 항등 — refiner 미구현이어도 파이프라인이 깨지지 않도록.)
|
|
28
|
+
*/
|
|
29
|
+
class IdentityLayoutRefiner {
|
|
30
|
+
constructor() {
|
|
31
|
+
this.id = 'identity';
|
|
32
|
+
}
|
|
33
|
+
refine(entities, _ctx) {
|
|
34
|
+
return entities;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.IdentityLayoutRefiner = IdentityLayoutRefiner;
|
|
38
|
+
//# sourceMappingURL=stubs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stubs.js","sourceRoot":"","sources":["../../../server/service/extraction/stubs.ts"],"names":[],"mappings":";;;AAkBA,MAAM,EAAE,GAAG,CAAC,IAAY,EAAS,EAAE;IACjC,MAAM,IAAI,KAAK,CAAC,6BAA6B,IAAI,sCAAsC,CAAC,CAAA;AAC1F,CAAC,CAAA;AAED,MAAa,mBAAmB;IAAhC;QACW,OAAE,GAAG,aAAa,CAAA;IAI7B,CAAC;IAHC,KAAK,CAAC,OAAO;QACX,OAAO,EAAE,CAAC,gDAAgD,CAAC,CAAA;IAC7D,CAAC;CACF;AALD,kDAKC;AAED,MAAa,sBAAsB;IAAnC;QACW,OAAE,GAAG,WAAW,CAAA;IAI3B,CAAC;IAHC,KAAK,CAAC,OAAO;QACX,OAAO,EAAE,CAAC,wDAAwD,CAAC,CAAA;IACrE,CAAC;CACF;AALD,wDAKC;AAED;;;GAGG;AACH,MAAa,qBAAqB;IAAlC;QACW,OAAE,GAAG,UAAU,CAAA;IAI1B,CAAC;IAHC,MAAM,CAAC,QAA2B,EAAE,IAAwB;QAC1D,OAAO,QAAQ,CAAA;IACjB,CAAC;CACF;AALD,sDAKC","sourcesContent":["/*\n * 스캐폴딩 stub 구현 — 구조가 컴파일·배선되도록 자리만 잡는다.\n * 남은 stub (본격 테스트 단계에서 소스 적합 impl 로 교체):\n * CvGeometryExtractor → OpenCV(선/사각/연결성분) 또는 Python 사이드카\n * CloudOcrLabelExtractor → GCP Vision / AWS Textract / Tesseract\n * 미구현 stub 은 호출 시 명확히 throw — 실수로 live 경로에 붙어도 즉시 드러난다.\n * (실구현: VectorGeometryExtractor / GridSnapLayoutRefiner / VlmRecognizer / LabelScaleCalibrator)\n */\nimport type { UniversalEntity } from '../types/index'\nimport type {\n GeometryExtractor,\n LabelExtractor,\n LayoutRefiner,\n GeometricPrimitive,\n Label,\n ExtractionContext\n} from './contracts'\n\nconst NI = (what: string): never => {\n throw new Error(`[board-import/extraction] ${what} 미구현 (스캐폴딩) — Phase B 본격 테스트 단계에서 구현`)\n}\n\nexport class CvGeometryExtractor implements GeometryExtractor {\n readonly id = 'cv-geometry'\n async extract(): Promise<GeometricPrimitive[]> {\n return NI('CvGeometryExtractor.extract (OpenCV 선/사각/연결성분)')\n }\n}\n\nexport class CloudOcrLabelExtractor implements LabelExtractor {\n readonly id = 'cloud-ocr'\n async extract(): Promise<Label[]> {\n return NI('CloudOcrLabelExtractor.extract (GCP Vision / Textract)')\n }\n}\n\n/**\n * 레이아웃 정리 stub — 정렬/스냅/겹침해소는 미구현이라 *그대로 통과*(no-op).\n * (throw 가 아니라 안전한 항등 — refiner 미구현이어도 파이프라인이 깨지지 않도록.)\n */\nexport class IdentityLayoutRefiner implements LayoutRefiner {\n readonly id = 'identity'\n refine(entities: UniversalEntity[], _ctx?: ExtractionContext): UniversalEntity[] {\n return entities\n }\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AdapterInput } from '../types/index';
|
|
2
|
+
import type { GeometryExtractor, GeometricPrimitive, ExtractionContext } from './contracts';
|
|
3
|
+
export declare class VectorGeometryExtractor implements GeometryExtractor {
|
|
4
|
+
readonly id = "vector-geometry";
|
|
5
|
+
private readonly dxf;
|
|
6
|
+
extract(input: AdapterInput, _ctx?: ExtractionContext): Promise<GeometricPrimitive[]>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VectorGeometryExtractor = void 0;
|
|
4
|
+
const dxf_adapter_1 = require("../adapters/dxf-adapter");
|
|
5
|
+
class VectorGeometryExtractor {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.id = 'vector-geometry';
|
|
8
|
+
this.dxf = new dxf_adapter_1.DxfAdapter();
|
|
9
|
+
}
|
|
10
|
+
async extract(input, _ctx) {
|
|
11
|
+
// 현재 DXF 만 지원. (SVG/IFC 확장 시 detect 로 어댑터 선택.)
|
|
12
|
+
const result = await this.dxf.parse(input);
|
|
13
|
+
return result.entities.map(e => ({
|
|
14
|
+
kind: e.kind === 'path' ? 'line' : 'rect',
|
|
15
|
+
bbox: e.bbox,
|
|
16
|
+
entity: e // 무손실 보존 — fuse 가 그대로 사용
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.VectorGeometryExtractor = VectorGeometryExtractor;
|
|
21
|
+
//# sourceMappingURL=vector-geometry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vector-geometry.js","sourceRoot":"","sources":["../../../server/service/extraction/vector-geometry.ts"],"names":[],"mappings":";;;AAUA,yDAAoD;AAEpD,MAAa,uBAAuB;IAApC;QACW,OAAE,GAAG,iBAAiB,CAAA;QACd,QAAG,GAAG,IAAI,wBAAU,EAAE,CAAA;IAWzC,CAAC;IATC,KAAK,CAAC,OAAO,CAAC,KAAmB,EAAE,IAAwB;QACzD,+CAA+C;QAC/C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QAC1C,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC/B,IAAI,EAAE,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAE,MAAgB,CAAC,CAAC,CAAE,MAAgB;YAC/D,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,MAAM,EAAE,CAAC,CAAC,yBAAyB;SACpC,CAAC,CAAC,CAAA;IACL,CAAC;CACF;AAbD,0DAaC","sourcesContent":["/*\n * VectorGeometryExtractor — 벡터 소스(DXF 등)를 *직접 파싱*해 정밀 기하 추출.\n * Phase Board-AI §3.8 의 \"벡터 = 1순위 정밀 경로\". 기존 DxfAdapter 를 흡수(델리게이트):\n * DXF entity 는 이미 위치+크기+회전+attribs(name/layer/shape) 를 가진 UniversalEntity 이므로,\n * 각 엔티티를 GeometricPrimitive.entity 로 그대로 실어 *무손실* 전달한다.\n *\n * 추후: SVG / IFC 어댑터를 detect 로 분기해 같은 방식으로 흡수.\n */\nimport type { AdapterInput } from '../types/index'\nimport type { GeometryExtractor, GeometricPrimitive, ExtractionContext } from './contracts'\nimport { DxfAdapter } from '../adapters/dxf-adapter'\n\nexport class VectorGeometryExtractor implements GeometryExtractor {\n readonly id = 'vector-geometry'\n private readonly dxf = new DxfAdapter()\n\n async extract(input: AdapterInput, _ctx?: ExtractionContext): Promise<GeometricPrimitive[]> {\n // 현재 DXF 만 지원. (SVG/IFC 확장 시 detect 로 어댑터 선택.)\n const result = await this.dxf.parse(input)\n return result.entities.map(e => ({\n kind: e.kind === 'path' ? ('line' as const) : ('rect' as const),\n bbox: e.bbox,\n entity: e // 무손실 보존 — fuse 가 그대로 사용\n }))\n }\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type AIClient } from '@things-factory/ai-client-base';
|
|
2
|
+
import type { Recognizer, RecognitionResult, GeometricPrimitive, ExtractionContext } from './contracts';
|
|
3
|
+
interface RawRecognition {
|
|
4
|
+
componentType?: string | null;
|
|
5
|
+
confidence?: number;
|
|
6
|
+
attribs?: Record<string, any>;
|
|
7
|
+
}
|
|
8
|
+
export declare class VlmRecognizer implements Recognizer {
|
|
9
|
+
private readonly client?;
|
|
10
|
+
readonly id = "vlm-recognizer";
|
|
11
|
+
constructor(client?: AIClient);
|
|
12
|
+
recognize(region: GeometricPrimitive, ctx?: ExtractionContext): Promise<RecognitionResult>;
|
|
13
|
+
}
|
|
14
|
+
/** region 신호 + 카탈로그 → 인식 프롬프트. */
|
|
15
|
+
export declare function buildPrompt(region: GeometricPrimitive, ctx?: ExtractionContext): string;
|
|
16
|
+
/** 응답 정규화 + 카탈로그 그라운딩 + confidence 클램프. */
|
|
17
|
+
export declare function normalize(raw: RawRecognition | null | undefined, catalog?: string[]): RecognitionResult;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VlmRecognizer = void 0;
|
|
4
|
+
exports.buildPrompt = buildPrompt;
|
|
5
|
+
exports.normalize = normalize;
|
|
6
|
+
/*
|
|
7
|
+
* VlmRecognizer — region 이 *어떤 컴포넌트인지* 식별 (Phase Board-AI §3.8 의 Recognizer).
|
|
8
|
+
*
|
|
9
|
+
* 좌표/레이아웃은 절대 추정하지 않는다(그건 Geometry/CV 의 몫). VLM/LLM 의 강점인
|
|
10
|
+
* *의미 인식* 만: 주어진 신호(라벨·모양·크기·레이어) + 카탈로그 → componentType.
|
|
11
|
+
* 카탈로그(ctx.catalog) 가 있으면 그 안의 값만 허용(그라운딩).
|
|
12
|
+
*
|
|
13
|
+
* provider 는 ai-client-base 의 AIClient. 생성자 주입 가능(테스트), 미주입 시 기본 클라이언트.
|
|
14
|
+
*/
|
|
15
|
+
const ai_client_base_1 = require("@things-factory/ai-client-base");
|
|
16
|
+
const SYSTEM = '당신은 시설 도면의 한 영역(region)이 어떤 보드 컴포넌트 type 인지 판정하는 분류기다. ' +
|
|
17
|
+
'좌표/위치/크기를 추정하지 말고, 주어진 신호(라벨/모양/크기/레이어)와 카탈로그만으로 *type* 만 판정한다. ' +
|
|
18
|
+
'STRICT JSON 만 반환(마크다운/주석 금지).';
|
|
19
|
+
class VlmRecognizer {
|
|
20
|
+
constructor(client) {
|
|
21
|
+
this.client = client;
|
|
22
|
+
this.id = 'vlm-recognizer';
|
|
23
|
+
}
|
|
24
|
+
async recognize(region, ctx) {
|
|
25
|
+
const client = this.client ?? (0, ai_client_base_1.getDefaultAIClient)();
|
|
26
|
+
if (!client) {
|
|
27
|
+
throw new Error('[board-import/extraction] VlmRecognizer 는 AIClient 가 필요합니다 (setDefaultAIClient / config.aiClient).');
|
|
28
|
+
}
|
|
29
|
+
const raw = await client.generateJSON(buildPrompt(region, ctx), {
|
|
30
|
+
systemPrompt: SYSTEM,
|
|
31
|
+
temperature: 0,
|
|
32
|
+
maxTokens: 300
|
|
33
|
+
});
|
|
34
|
+
return normalize(raw, ctx?.catalog);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.VlmRecognizer = VlmRecognizer;
|
|
38
|
+
/** region 신호 + 카탈로그 → 인식 프롬프트. */
|
|
39
|
+
function buildPrompt(region, ctx) {
|
|
40
|
+
const w = region.bbox.max.x - region.bbox.min.x;
|
|
41
|
+
const h = region.bbox.max.y - region.bbox.min.y;
|
|
42
|
+
const aspect = h !== 0 ? w / h : 0;
|
|
43
|
+
const a = region.entity?.attribs ?? {};
|
|
44
|
+
const m = region.meta ?? {};
|
|
45
|
+
const signals = [`kind=${region.kind}`, `size=${round(w)}x${round(h)}`, `aspect=${aspect.toFixed(2)}`];
|
|
46
|
+
const name = a.name ?? m.name ?? m.label;
|
|
47
|
+
if (name)
|
|
48
|
+
signals.push(`label="${name}"`);
|
|
49
|
+
if (a.shape ?? m.shape)
|
|
50
|
+
signals.push(`shape=${a.shape ?? m.shape}`);
|
|
51
|
+
if (a.layer ?? m.layer)
|
|
52
|
+
signals.push(`layer=${a.layer ?? m.layer}`);
|
|
53
|
+
if (a.text ?? m.text)
|
|
54
|
+
signals.push(`text="${a.text ?? m.text}"`);
|
|
55
|
+
const catalogLine = ctx?.catalog && ctx.catalog.length
|
|
56
|
+
? `\n카탈로그(이 중에서만 선택, 맞는 게 없으면 componentType=null):\n${ctx.catalog.join(', ')}`
|
|
57
|
+
: '\n카탈로그 미지정 — 가장 적절한 컴포넌트 type 을 자유롭게 판정.';
|
|
58
|
+
const hint = ctx?.userPrompt ? `\n사용자 힌트: ${ctx.userPrompt}` : '';
|
|
59
|
+
return (`다음 영역의 신호로 보드 컴포넌트 type 을 판정하라.\n신호: ${signals.join(', ')}${catalogLine}${hint}\n` +
|
|
60
|
+
`반환 JSON: { "componentType": string|null, "confidence": 0..1, "attribs": { ... } }`);
|
|
61
|
+
}
|
|
62
|
+
/** 응답 정규화 + 카탈로그 그라운딩 + confidence 클램프. */
|
|
63
|
+
function normalize(raw, catalog) {
|
|
64
|
+
let type = typeof raw?.componentType === 'string' && raw.componentType.trim() ? raw.componentType.trim() : undefined;
|
|
65
|
+
// 카탈로그 있으면 그 안의 값만 허용 — 그라운딩(환각 type 차단).
|
|
66
|
+
if (type && catalog && catalog.length && !catalog.includes(type))
|
|
67
|
+
type = undefined;
|
|
68
|
+
let conf = typeof raw?.confidence === 'number' && Number.isFinite(raw.confidence) ? raw.confidence : 0.5;
|
|
69
|
+
conf = Math.max(0, Math.min(1, conf));
|
|
70
|
+
if (!type)
|
|
71
|
+
conf = Math.min(conf, 0.3); // 타입 미상이면 신뢰도 상한
|
|
72
|
+
const attribs = raw?.attribs && typeof raw.attribs === 'object' ? raw.attribs : undefined;
|
|
73
|
+
return { componentType: type, confidence: conf, attribs };
|
|
74
|
+
}
|
|
75
|
+
function round(n) {
|
|
76
|
+
return Math.round(n * 100) / 100;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=vlm-recognizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vlm-recognizer.js","sourceRoot":"","sources":["../../../server/service/extraction/vlm-recognizer.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * VlmRecognizer — region 이 *어떤 컴포넌트인지* 식별 (Phase Board-AI §3.8 의 Recognizer).\n *\n * 좌표/레이아웃은 절대 추정하지 않는다(그건 Geometry/CV 의 몫). VLM/LLM 의 강점인\n * *의미 인식* 만: 주어진 신호(라벨·모양·크기·레이어) + 카탈로그 → componentType.\n * 카탈로그(ctx.catalog) 가 있으면 그 안의 값만 허용(그라운딩).\n *\n * provider 는 ai-client-base 의 AIClient. 생성자 주입 가능(테스트), 미주입 시 기본 클라이언트.\n */\nimport { getDefaultAIClient, type AIClient } from '@things-factory/ai-client-base'\nimport type { Recognizer, RecognitionResult, GeometricPrimitive, ExtractionContext } from './contracts'\n\ninterface RawRecognition {\n componentType?: string | null\n confidence?: number\n attribs?: Record<string, any>\n}\n\nconst SYSTEM =\n '당신은 시설 도면의 한 영역(region)이 어떤 보드 컴포넌트 type 인지 판정하는 분류기다. ' +\n '좌표/위치/크기를 추정하지 말고, 주어진 신호(라벨/모양/크기/레이어)와 카탈로그만으로 *type* 만 판정한다. ' +\n 'STRICT JSON 만 반환(마크다운/주석 금지).'\n\nexport class VlmRecognizer implements Recognizer {\n readonly id = 'vlm-recognizer'\n\n constructor(private readonly client?: AIClient) {}\n\n async recognize(region: GeometricPrimitive, ctx?: ExtractionContext): Promise<RecognitionResult> {\n const client = this.client ?? getDefaultAIClient()\n if (!client) {\n throw new Error('[board-import/extraction] VlmRecognizer 는 AIClient 가 필요합니다 (setDefaultAIClient / config.aiClient).')\n }\n const raw = await client.generateJSON<RawRecognition>(buildPrompt(region, ctx), {\n systemPrompt: SYSTEM,\n temperature: 0,\n maxTokens: 300\n })\n return normalize(raw, ctx?.catalog)\n }\n}\n\n/** region 신호 + 카탈로그 → 인식 프롬프트. */\nexport function buildPrompt(region: GeometricPrimitive, ctx?: ExtractionContext): string {\n const w = region.bbox.max.x - region.bbox.min.x\n const h = region.bbox.max.y - region.bbox.min.y\n const aspect = h !== 0 ? w / h : 0\n const a = region.entity?.attribs ?? {}\n const m = region.meta ?? {}\n\n const signals: string[] = [`kind=${region.kind}`, `size=${round(w)}x${round(h)}`, `aspect=${aspect.toFixed(2)}`]\n const name = a.name ?? m.name ?? m.label\n if (name) signals.push(`label=\"${name}\"`)\n if (a.shape ?? m.shape) signals.push(`shape=${a.shape ?? m.shape}`)\n if (a.layer ?? m.layer) signals.push(`layer=${a.layer ?? m.layer}`)\n if (a.text ?? m.text) signals.push(`text=\"${a.text ?? m.text}\"`)\n\n const catalogLine =\n ctx?.catalog && ctx.catalog.length\n ? `\\n카탈로그(이 중에서만 선택, 맞는 게 없으면 componentType=null):\\n${ctx.catalog.join(', ')}`\n : '\\n카탈로그 미지정 — 가장 적절한 컴포넌트 type 을 자유롭게 판정.'\n const hint = ctx?.userPrompt ? `\\n사용자 힌트: ${ctx.userPrompt}` : ''\n\n return (\n `다음 영역의 신호로 보드 컴포넌트 type 을 판정하라.\\n신호: ${signals.join(', ')}${catalogLine}${hint}\\n` +\n `반환 JSON: { \"componentType\": string|null, \"confidence\": 0..1, \"attribs\": { ... } }`\n )\n}\n\n/** 응답 정규화 + 카탈로그 그라운딩 + confidence 클램프. */\nexport function normalize(raw: RawRecognition | null | undefined, catalog?: string[]): RecognitionResult {\n let type =\n typeof raw?.componentType === 'string' && raw.componentType.trim() ? raw.componentType.trim() : undefined\n // 카탈로그 있으면 그 안의 값만 허용 — 그라운딩(환각 type 차단).\n if (type && catalog && catalog.length && !catalog.includes(type)) type = undefined\n\n let conf = typeof raw?.confidence === 'number' && Number.isFinite(raw.confidence) ? raw.confidence : 0.5\n conf = Math.max(0, Math.min(1, conf))\n if (!type) conf = Math.min(conf, 0.3) // 타입 미상이면 신뢰도 상한\n\n const attribs = raw?.attribs && typeof raw.attribs === 'object' ? raw.attribs : undefined\n return { componentType: type, confidence: conf, attribs }\n}\n\nfunction round(n: number): number {\n return Math.round(n * 100) / 100\n}\n"]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* board-import 의 ResolverContext 무관 service-level 함수.
|
|
3
|
+
*
|
|
4
|
+
* import-session-resolver 의 mutation/query 핵심 로직을 typeorm getRepository /
|
|
5
|
+
* ResolverContext 외부 의존성을 인자로 분리해 단위 테스트 + LLM tool builder 에서
|
|
6
|
+
* 재사용 가능하도록 추출. materializeFromSession 의 패턴 동일.
|
|
7
|
+
*
|
|
8
|
+
* 호출자 책임:
|
|
9
|
+
* - resolver: getRepository / context.state 에서 도메인/유저 추출 후 deps 채워 호출
|
|
10
|
+
* - import-tools.ts: ToolDispatchHelpers 가 builder 호출 시 ctx 에서 추출
|
|
11
|
+
*/
|
|
12
|
+
import type { ImportSession } from './import-session';
|
|
13
|
+
export interface SessionRepoLike {
|
|
14
|
+
findOneBy(criteria: any): Promise<ImportSession | null>;
|
|
15
|
+
save(entity: any): Promise<ImportSession>;
|
|
16
|
+
find(options: any): Promise<ImportSession[]>;
|
|
17
|
+
}
|
|
18
|
+
export interface ImportActionsContext {
|
|
19
|
+
domain: {
|
|
20
|
+
id: string;
|
|
21
|
+
[k: string]: any;
|
|
22
|
+
};
|
|
23
|
+
user: any;
|
|
24
|
+
}
|
|
25
|
+
export interface StartImportJobInput {
|
|
26
|
+
attachmentId: string;
|
|
27
|
+
chatSessionId?: string;
|
|
28
|
+
scopes?: string[];
|
|
29
|
+
normalizeOrigin?: boolean;
|
|
30
|
+
flipY?: boolean;
|
|
31
|
+
scale?: number;
|
|
32
|
+
applyBinding?: boolean;
|
|
33
|
+
parseOptions?: any;
|
|
34
|
+
/**
|
|
35
|
+
* 사용자 자유서술 — 도면 설명 / 강조 부분 / 분류 힌트 등.
|
|
36
|
+
* VLM 이 view type 분류 + entity 추출 양쪽 패스에 모두 추가 컨텍스트로 사용.
|
|
37
|
+
*/
|
|
38
|
+
userPrompt?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 비동기 import job 시작. ImportSession 즉시 반환, 백그라운드에서 worker 실행.
|
|
42
|
+
*
|
|
43
|
+
* resolver 와 LLM tool builder 양쪽이 공유하는 진입점. side-effect 인 worker 가동도
|
|
44
|
+
* 본 함수에 묶음 — 사용자 입장에선 "단일 호출 = import 시작" 의미.
|
|
45
|
+
*
|
|
46
|
+
* `startWorker` 는 의존성 주입 — default 는 setImmediate(runImportJob). 단위 테스트는
|
|
47
|
+
* noop 주입해 worker chain 이 jest 환경에서 평가되지 않도록 한다 (typeorm DataSource init
|
|
48
|
+
* 안 된 환경에서 entity decorator 평가 시 fail 회피).
|
|
49
|
+
*/
|
|
50
|
+
export declare function startImportJobAsync(input: StartImportJobInput, context: ImportActionsContext, sessionRepo: SessionRepoLike, startWorker?: (sessionId: string) => void): Promise<ImportSession>;
|
|
51
|
+
/**
|
|
52
|
+
* 단일 ImportSession 조회 (도메인 격리). 미존재면 null.
|
|
53
|
+
*/
|
|
54
|
+
export declare function fetchImportSession(sessionId: string, context: ImportActionsContext, sessionRepo: SessionRepoLike): Promise<ImportSession | null>;
|
|
55
|
+
/**
|
|
56
|
+
* 한 ChatSession 안에서 발동된 ImportSession 목록 (최신순 50개). board-ai 의 chat 흐름이
|
|
57
|
+
* "이 chat 의 import 들 보여줘" 같은 호출에서 사용.
|
|
58
|
+
*/
|
|
59
|
+
export declare function listImportSessionsByChatSession(chatSessionId: string, context: ImportActionsContext, sessionRepo: SessionRepoLike): Promise<ImportSession[]>;
|
|
60
|
+
/**
|
|
61
|
+
* 등록된 FormatAdapter 들의 능력 정보. context 무관 (인스턴스만 inspect).
|
|
62
|
+
* 외부 RPC 의존성도 없어 LLM 이 안전하게 호출 가능한 read tool.
|
|
63
|
+
*/
|
|
64
|
+
export interface AdapterInfo {
|
|
65
|
+
format: string;
|
|
66
|
+
supportedExtensions: string[];
|
|
67
|
+
capabilities: {
|
|
68
|
+
has2D: boolean;
|
|
69
|
+
has3D: boolean;
|
|
70
|
+
hasSemanticLabels: boolean;
|
|
71
|
+
hasTextLabels: boolean;
|
|
72
|
+
hasAttributes: boolean;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export declare function describeAdapter(adapter: {
|
|
76
|
+
format: string;
|
|
77
|
+
supportedExtensions: string[];
|
|
78
|
+
capabilities: any;
|
|
79
|
+
}): AdapterInfo;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* board-import 의 ResolverContext 무관 service-level 함수.
|
|
4
|
+
*
|
|
5
|
+
* import-session-resolver 의 mutation/query 핵심 로직을 typeorm getRepository /
|
|
6
|
+
* ResolverContext 외부 의존성을 인자로 분리해 단위 테스트 + LLM tool builder 에서
|
|
7
|
+
* 재사용 가능하도록 추출. materializeFromSession 의 패턴 동일.
|
|
8
|
+
*
|
|
9
|
+
* 호출자 책임:
|
|
10
|
+
* - resolver: getRepository / context.state 에서 도메인/유저 추출 후 deps 채워 호출
|
|
11
|
+
* - import-tools.ts: ToolDispatchHelpers 가 builder 호출 시 ctx 에서 추출
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.startImportJobAsync = startImportJobAsync;
|
|
15
|
+
exports.fetchImportSession = fetchImportSession;
|
|
16
|
+
exports.listImportSessionsByChatSession = listImportSessionsByChatSession;
|
|
17
|
+
exports.describeAdapter = describeAdapter;
|
|
18
|
+
// ── service 함수들 ────────────────────────────────────────────────────
|
|
19
|
+
/**
|
|
20
|
+
* 비동기 import job 시작. ImportSession 즉시 반환, 백그라운드에서 worker 실행.
|
|
21
|
+
*
|
|
22
|
+
* resolver 와 LLM tool builder 양쪽이 공유하는 진입점. side-effect 인 worker 가동도
|
|
23
|
+
* 본 함수에 묶음 — 사용자 입장에선 "단일 호출 = import 시작" 의미.
|
|
24
|
+
*
|
|
25
|
+
* `startWorker` 는 의존성 주입 — default 는 setImmediate(runImportJob). 단위 테스트는
|
|
26
|
+
* noop 주입해 worker chain 이 jest 환경에서 평가되지 않도록 한다 (typeorm DataSource init
|
|
27
|
+
* 안 된 환경에서 entity decorator 평가 시 fail 회피).
|
|
28
|
+
*/
|
|
29
|
+
async function startImportJobAsync(input, context, sessionRepo, startWorker = defaultStartWorker) {
|
|
30
|
+
const session = await sessionRepo.save({
|
|
31
|
+
domain: context.domain,
|
|
32
|
+
attachmentId: input.attachmentId,
|
|
33
|
+
chatSessionId: input.chatSessionId,
|
|
34
|
+
status: 'queued',
|
|
35
|
+
progress: 0,
|
|
36
|
+
message: 'Queued',
|
|
37
|
+
optionsJson: JSON.stringify({
|
|
38
|
+
scopes: input.scopes,
|
|
39
|
+
normalizeOrigin: input.normalizeOrigin,
|
|
40
|
+
flipY: input.flipY,
|
|
41
|
+
scale: input.scale,
|
|
42
|
+
applyBinding: input.applyBinding,
|
|
43
|
+
parseOptions: input.parseOptions,
|
|
44
|
+
userPrompt: input.userPrompt
|
|
45
|
+
}),
|
|
46
|
+
creator: context.user
|
|
47
|
+
});
|
|
48
|
+
// 백그라운드 worker 시작 — 의존성 주입으로 분리 (테스트 격리 + lazy require).
|
|
49
|
+
startWorker(session.id);
|
|
50
|
+
return session;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Default startWorker — setImmediate(runImportJob). worker module 은 lazy require 로
|
|
54
|
+
* module-level 평가 chain (attachment-base STORAGE, pipeline 의 adapters 등) 을 회피.
|
|
55
|
+
*/
|
|
56
|
+
function defaultStartWorker(sessionId) {
|
|
57
|
+
setImmediate(() => {
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
59
|
+
const { runImportJob } = require('./import-worker');
|
|
60
|
+
runImportJob(sessionId).catch((err) => {
|
|
61
|
+
console.error('[board-import] runImportJob unhandled:', err);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 단일 ImportSession 조회 (도메인 격리). 미존재면 null.
|
|
67
|
+
*/
|
|
68
|
+
async function fetchImportSession(sessionId, context, sessionRepo) {
|
|
69
|
+
return await sessionRepo.findOneBy({
|
|
70
|
+
id: sessionId,
|
|
71
|
+
domain: { id: context.domain.id }
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 한 ChatSession 안에서 발동된 ImportSession 목록 (최신순 50개). board-ai 의 chat 흐름이
|
|
76
|
+
* "이 chat 의 import 들 보여줘" 같은 호출에서 사용.
|
|
77
|
+
*/
|
|
78
|
+
async function listImportSessionsByChatSession(chatSessionId, context, sessionRepo) {
|
|
79
|
+
return await sessionRepo.find({
|
|
80
|
+
where: { chatSessionId, domain: { id: context.domain.id } },
|
|
81
|
+
order: { createdAt: 'DESC' },
|
|
82
|
+
take: 50
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function describeAdapter(adapter) {
|
|
86
|
+
return {
|
|
87
|
+
format: adapter.format,
|
|
88
|
+
supportedExtensions: [...adapter.supportedExtensions],
|
|
89
|
+
capabilities: {
|
|
90
|
+
has2D: !!adapter.capabilities.has2D,
|
|
91
|
+
has3D: !!adapter.capabilities.has3D,
|
|
92
|
+
hasSemanticLabels: !!adapter.capabilities.hasSemanticLabels,
|
|
93
|
+
hasTextLabels: !!adapter.capabilities.hasTextLabels,
|
|
94
|
+
hasAttributes: !!adapter.capabilities.hasAttributes
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=import-actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import-actions.js","sourceRoot":"","sources":["../../../server/service/import-session/import-actions.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;AA+CH,kDA4BC;AAmBD,gDASC;AAMD,0EAUC;AAkBD,0CAgBC;AAtHD,sEAAsE;AAEtE;;;;;;;;;GASG;AACI,KAAK,UAAU,mBAAmB,CACvC,KAA0B,EAC1B,OAA6B,EAC7B,WAA4B,EAC5B,cAA2C,kBAAkB;IAE7D,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC;QACrC,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,MAAM,EAAE,QAAQ;QAChB,QAAQ,EAAE,CAAC;QACX,OAAO,EAAE,QAAQ;QACjB,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC;YAC1B,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,eAAe,EAAE,KAAK,CAAC,eAAe;YACtC,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,YAAY,EAAE,KAAK,CAAC,YAAY;YAChC,YAAY,EAAE,KAAK,CAAC,YAAY;YAChC,UAAU,EAAE,KAAK,CAAC,UAAU;SAC7B,CAAC;QACF,OAAO,EAAE,OAAO,CAAC,IAAI;KACf,CAAC,CAAA;IAET,yDAAyD;IACzD,WAAW,CAAC,OAAO,CAAC,EAAG,CAAC,CAAA;IACxB,OAAO,OAAO,CAAA;AAChB,CAAC;AAED;;;GAGG;AACH,SAAS,kBAAkB,CAAC,SAAiB;IAC3C,YAAY,CAAC,GAAG,EAAE;QAChB,8DAA8D;QAC9D,MAAM,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAA;QACnD,YAAY,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,GAAU,EAAE,EAAE;YAC3C,OAAO,CAAC,KAAK,CAAC,wCAAwC,EAAE,GAAG,CAAC,CAAA;QAC9D,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;GAEG;AACI,KAAK,UAAU,kBAAkB,CACtC,SAAiB,EACjB,OAA6B,EAC7B,WAA4B;IAE5B,OAAO,MAAM,WAAW,CAAC,SAAS,CAAC;QACjC,EAAE,EAAE,SAAS;QACb,MAAM,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE;KAClC,CAAC,CAAA;AACJ,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,+BAA+B,CACnD,aAAqB,EACrB,OAA6B,EAC7B,WAA4B;IAE5B,OAAO,MAAM,WAAW,CAAC,IAAI,CAAC;QAC5B,KAAK,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE;QAC3D,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;QAC5B,IAAI,EAAE,EAAE;KACT,CAAC,CAAA;AACJ,CAAC;AAkBD,SAAgB,eAAe,CAAC,OAI/B;IACC,OAAO;QACL,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,mBAAmB,EAAE,CAAC,GAAG,OAAO,CAAC,mBAAmB,CAAC;QACrD,YAAY,EAAE;YACZ,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK;YACnC,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK;YACnC,iBAAiB,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,iBAAiB;YAC3D,aAAa,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,aAAa;YACnD,aAAa,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,aAAa;SACpD;KACF,CAAA;AACH,CAAC","sourcesContent":["/**\n * board-import 의 ResolverContext 무관 service-level 함수.\n *\n * import-session-resolver 의 mutation/query 핵심 로직을 typeorm getRepository /\n * ResolverContext 외부 의존성을 인자로 분리해 단위 테스트 + LLM tool builder 에서\n * 재사용 가능하도록 추출. materializeFromSession 의 패턴 동일.\n *\n * 호출자 책임:\n * - resolver: getRepository / context.state 에서 도메인/유저 추출 후 deps 채워 호출\n * - import-tools.ts: ToolDispatchHelpers 가 builder 호출 시 ctx 에서 추출\n */\n\n// type-only import — module level 에서 typeorm decorator chain 을 평가하지 않는다.\n// 단위 테스트 환경이 typeorm DataSource init 전에도 service 함수 검증할 수 있게 함.\nimport type { ImportSession } from './import-session'\n\n// ── 입력/의존성 타입 ──────────────────────────────────────────────────\n\nexport interface SessionRepoLike {\n findOneBy(criteria: any): Promise<ImportSession | null>\n save(entity: any): Promise<ImportSession>\n find(options: any): Promise<ImportSession[]>\n}\n\nexport interface ImportActionsContext {\n domain: { id: string; [k: string]: any }\n user: any\n}\n\nexport interface StartImportJobInput {\n attachmentId: string\n chatSessionId?: string\n scopes?: string[]\n normalizeOrigin?: boolean\n flipY?: boolean\n scale?: number\n applyBinding?: boolean\n parseOptions?: any\n /**\n * 사용자 자유서술 — 도면 설명 / 강조 부분 / 분류 힌트 등.\n * VLM 이 view type 분류 + entity 추출 양쪽 패스에 모두 추가 컨텍스트로 사용.\n */\n userPrompt?: string\n}\n\n// ── service 함수들 ────────────────────────────────────────────────────\n\n/**\n * 비동기 import job 시작. ImportSession 즉시 반환, 백그라운드에서 worker 실행.\n *\n * resolver 와 LLM tool builder 양쪽이 공유하는 진입점. side-effect 인 worker 가동도\n * 본 함수에 묶음 — 사용자 입장에선 \"단일 호출 = import 시작\" 의미.\n *\n * `startWorker` 는 의존성 주입 — default 는 setImmediate(runImportJob). 단위 테스트는\n * noop 주입해 worker chain 이 jest 환경에서 평가되지 않도록 한다 (typeorm DataSource init\n * 안 된 환경에서 entity decorator 평가 시 fail 회피).\n */\nexport async function startImportJobAsync(\n input: StartImportJobInput,\n context: ImportActionsContext,\n sessionRepo: SessionRepoLike,\n startWorker: (sessionId: string) => void = defaultStartWorker\n): Promise<ImportSession> {\n const session = await sessionRepo.save({\n domain: context.domain,\n attachmentId: input.attachmentId,\n chatSessionId: input.chatSessionId,\n status: 'queued',\n progress: 0,\n message: 'Queued',\n optionsJson: JSON.stringify({\n scopes: input.scopes,\n normalizeOrigin: input.normalizeOrigin,\n flipY: input.flipY,\n scale: input.scale,\n applyBinding: input.applyBinding,\n parseOptions: input.parseOptions,\n userPrompt: input.userPrompt\n }),\n creator: context.user\n } as any)\n\n // 백그라운드 worker 시작 — 의존성 주입으로 분리 (테스트 격리 + lazy require).\n startWorker(session.id!)\n return session\n}\n\n/**\n * Default startWorker — setImmediate(runImportJob). worker module 은 lazy require 로\n * module-level 평가 chain (attachment-base STORAGE, pipeline 의 adapters 등) 을 회피.\n */\nfunction defaultStartWorker(sessionId: string): void {\n setImmediate(() => {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const { runImportJob } = require('./import-worker')\n runImportJob(sessionId).catch((err: Error) => {\n console.error('[board-import] runImportJob unhandled:', err)\n })\n })\n}\n\n/**\n * 단일 ImportSession 조회 (도메인 격리). 미존재면 null.\n */\nexport async function fetchImportSession(\n sessionId: string,\n context: ImportActionsContext,\n sessionRepo: SessionRepoLike\n): Promise<ImportSession | null> {\n return await sessionRepo.findOneBy({\n id: sessionId,\n domain: { id: context.domain.id }\n })\n}\n\n/**\n * 한 ChatSession 안에서 발동된 ImportSession 목록 (최신순 50개). board-ai 의 chat 흐름이\n * \"이 chat 의 import 들 보여줘\" 같은 호출에서 사용.\n */\nexport async function listImportSessionsByChatSession(\n chatSessionId: string,\n context: ImportActionsContext,\n sessionRepo: SessionRepoLike\n): Promise<ImportSession[]> {\n return await sessionRepo.find({\n where: { chatSessionId, domain: { id: context.domain.id } },\n order: { createdAt: 'DESC' },\n take: 50\n })\n}\n\n/**\n * 등록된 FormatAdapter 들의 능력 정보. context 무관 (인스턴스만 inspect).\n * 외부 RPC 의존성도 없어 LLM 이 안전하게 호출 가능한 read tool.\n */\nexport interface AdapterInfo {\n format: string\n supportedExtensions: string[]\n capabilities: {\n has2D: boolean\n has3D: boolean\n hasSemanticLabels: boolean\n hasTextLabels: boolean\n hasAttributes: boolean\n }\n}\n\nexport function describeAdapter(adapter: {\n format: string\n supportedExtensions: string[]\n capabilities: any\n}): AdapterInfo {\n return {\n format: adapter.format,\n supportedExtensions: [...adapter.supportedExtensions],\n capabilities: {\n has2D: !!adapter.capabilities.has2D,\n has3D: !!adapter.capabilities.has3D,\n hasSemanticLabels: !!adapter.capabilities.hasSemanticLabels,\n hasTextLabels: !!adapter.capabilities.hasTextLabels,\n hasAttributes: !!adapter.capabilities.hasAttributes\n }\n }\n}\n"]}
|