David Ko commited on
Commit
b90e47b
ยท
1 Parent(s): 1aa3dcb

Add model-vector-db.html UI for YOLO/ViT/DETR model results vector DB storage and search

Browse files
Files changed (2) hide show
  1. api.py +5 -0
  2. frontend/build/model-vector-db.html +580 -0
api.py CHANGED
@@ -820,6 +820,11 @@ def object_detection_search_page():
820
  """Serve object detection search page"""
821
  return send_from_directory('frontend/build', 'object-detection-search.html')
822
 
 
 
 
 
 
823
  @app.route('/api/status', methods=['GET'])
824
  def status():
825
  return jsonify({
 
820
  """Serve object detection search page"""
821
  return send_from_directory('frontend/build', 'object-detection-search.html')
822
 
823
+ @app.route('/model-vector-db', methods=['GET'])
824
+ def model_vector_db_page():
825
+ """Serve model vector DB UI page"""
826
+ return send_from_directory('frontend/build', 'model-vector-db.html')
827
+
828
  @app.route('/api/status', methods=['GET'])
829
  def status():
830
  return jsonify({
frontend/build/model-vector-db.html ADDED
@@ -0,0 +1,580 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>๋ชจ๋ธ ๊ฒฐ๊ณผ ๋ฒกํ„ฐ DB ์ €์žฅ ๋ฐ ๊ฒ€์ƒ‰</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <style>
9
+ body {
10
+ padding: 20px;
11
+ background-color: #f8f9fa;
12
+ }
13
+ .card {
14
+ margin-bottom: 20px;
15
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
16
+ }
17
+ .result-image {
18
+ max-width: 100%;
19
+ height: auto;
20
+ border-radius: 4px;
21
+ }
22
+ .detection-box {
23
+ position: absolute;
24
+ border: 2px solid;
25
+ pointer-events: none;
26
+ box-sizing: border-box;
27
+ }
28
+ .image-container {
29
+ position: relative;
30
+ margin-bottom: 15px;
31
+ }
32
+ .model-btn {
33
+ margin-right: 10px;
34
+ margin-bottom: 10px;
35
+ }
36
+ .result-card {
37
+ transition: all 0.3s;
38
+ }
39
+ .result-card:hover {
40
+ transform: translateY(-5px);
41
+ }
42
+ .nav-tabs .nav-link.active {
43
+ font-weight: bold;
44
+ border-bottom: 3px solid #0d6efd;
45
+ }
46
+ .loading {
47
+ display: none;
48
+ text-align: center;
49
+ padding: 20px;
50
+ }
51
+ .search-result-item {
52
+ border-bottom: 1px solid #dee2e6;
53
+ padding-bottom: 15px;
54
+ margin-bottom: 15px;
55
+ }
56
+ .search-result-item:last-child {
57
+ border-bottom: none;
58
+ }
59
+ .similarity-badge {
60
+ font-size: 0.9rem;
61
+ }
62
+ </style>
63
+ </head>
64
+ <body>
65
+ <div class="container">
66
+ <h1 class="my-4 text-center">๋ชจ๋ธ ๊ฒฐ๊ณผ ๋ฒกํ„ฐ DB ์ €์žฅ ๋ฐ ๊ฒ€์ƒ‰</h1>
67
+
68
+ <ul class="nav nav-tabs mb-4" id="myTab" role="tablist">
69
+ <li class="nav-item" role="presentation">
70
+ <button class="nav-link active" id="detect-tab" data-bs-toggle="tab" data-bs-target="#detect" type="button" role="tab" aria-controls="detect" aria-selected="true">๊ฐ์ฒด ์ธ์‹ ๋ฐ ์ €์žฅ</button>
71
+ </li>
72
+ <li class="nav-item" role="presentation">
73
+ <button class="nav-link" id="search-tab" data-bs-toggle="tab" data-bs-target="#search" type="button" role="tab" aria-controls="search" aria-selected="false">๋ฒกํ„ฐ DB ๊ฒ€์ƒ‰</button>
74
+ </li>
75
+ </ul>
76
+
77
+ <div class="tab-content" id="myTabContent">
78
+ <!-- ๊ฐ์ฒด ์ธ์‹ ๋ฐ ์ €์žฅ ํƒญ -->
79
+ <div class="tab-pane fade show active" id="detect" role="tabpanel" aria-labelledby="detect-tab">
80
+ <div class="row">
81
+ <div class="col-md-6">
82
+ <div class="card">
83
+ <div class="card-header">
84
+ <h5 class="card-title mb-0">์ด๋ฏธ์ง€ ์—…๋กœ๋“œ</h5>
85
+ </div>
86
+ <div class="card-body">
87
+ <div class="mb-3">
88
+ <label for="imageUpload" class="form-label">์ด๋ฏธ์ง€ ์„ ํƒ</label>
89
+ <input class="form-control" type="file" id="imageUpload" accept="image/*">
90
+ </div>
91
+ <div class="image-container">
92
+ <img id="uploadedImage" class="img-fluid result-image" style="display: none;">
93
+ </div>
94
+ <div class="d-flex flex-wrap">
95
+ <button id="detectYolo" class="btn btn-primary model-btn">YOLOv8๋กœ ์ธ์‹</button>
96
+ <button id="detectDetr" class="btn btn-success model-btn">DETR๋กœ ์ธ์‹</button>
97
+ <button id="detectVit" class="btn btn-info model-btn">ViT๋กœ ๋ถ„๋ฅ˜</button>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </div>
102
+
103
+ <div class="col-md-6">
104
+ <div class="card">
105
+ <div class="card-header">
106
+ <h5 class="card-title mb-0">์ธ์‹ ๊ฒฐ๊ณผ</h5>
107
+ </div>
108
+ <div class="card-body">
109
+ <div class="image-container">
110
+ <img id="resultImage" class="img-fluid result-image" style="display: none;">
111
+ <div id="detectionBoxes"></div>
112
+ </div>
113
+ <div class="mb-3">
114
+ <label for="detectionResults" class="form-label">์ธ์‹๋œ ๊ฐ์ฒด</label>
115
+ <textarea class="form-control" id="detectionResults" rows="5" readonly></textarea>
116
+ </div>
117
+ <button id="saveToVectorDb" class="btn btn-warning" disabled>๋ฒกํ„ฐ DB์— ์ €์žฅ</button>
118
+ <div class="mt-3" id="saveResult"></div>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <div id="detectLoading" class="loading">
125
+ <div class="spinner-border text-primary" role="status">
126
+ <span class="visually-hidden">Loading...</span>
127
+ </div>
128
+ <p class="mt-2">์ฒ˜๋ฆฌ ์ค‘...</p>
129
+ </div>
130
+ </div>
131
+
132
+ <!-- ๋ฒกํ„ฐ DB ๊ฒ€์ƒ‰ ํƒญ -->
133
+ <div class="tab-pane fade" id="search" role="tabpanel" aria-labelledby="search-tab">
134
+ <div class="row">
135
+ <div class="col-md-5">
136
+ <div class="card">
137
+ <div class="card-header">
138
+ <h5 class="card-title mb-0">๊ฒ€์ƒ‰ ์˜ต์…˜</h5>
139
+ </div>
140
+ <div class="card-body">
141
+ <div class="mb-3">
142
+ <label class="form-label">๊ฒ€์ƒ‰ ๋ฐฉ๋ฒ•</label>
143
+ <div class="form-check">
144
+ <input class="form-check-input" type="radio" name="searchType" id="searchByImage" value="image" checked>
145
+ <label class="form-check-label" for="searchByImage">
146
+ ์ด๋ฏธ์ง€๋กœ ๊ฒ€์ƒ‰
147
+ </label>
148
+ </div>
149
+ <div class="form-check">
150
+ <input class="form-check-input" type="radio" name="searchType" id="searchByClass" value="class">
151
+ <label class="form-check-label" for="searchByClass">
152
+ ํด๋ž˜์Šค๋กœ ๊ฒ€์ƒ‰
153
+ </label>
154
+ </div>
155
+ </div>
156
+
157
+ <div id="imageSearchOptions">
158
+ <div class="mb-3">
159
+ <label for="searchImageUpload" class="form-label">์ด๋ฏธ์ง€ ์„ ํƒ</label>
160
+ <input class="form-control" type="file" id="searchImageUpload" accept="image/*">
161
+ </div>
162
+ <div class="image-container">
163
+ <img id="searchImage" class="img-fluid result-image" style="display: none;">
164
+ </div>
165
+ </div>
166
+
167
+ <div id="classSearchOptions" style="display: none;">
168
+ <div class="mb-3">
169
+ <label for="classInput" class="form-label">ํด๋ž˜์Šค ์ด๋ฆ„</label>
170
+ <input type="text" class="form-control" id="classInput" placeholder="์˜ˆ: person, car, dog...">
171
+ </div>
172
+ </div>
173
+
174
+ <div class="mb-3">
175
+ <label for="resultCount" class="form-label">๊ฒฐ๊ณผ ๊ฐœ์ˆ˜</label>
176
+ <select class="form-select" id="resultCount">
177
+ <option value="5">5๊ฐœ</option>
178
+ <option value="10">10๊ฐœ</option>
179
+ <option value="20">20๊ฐœ</option>
180
+ </select>
181
+ </div>
182
+
183
+ <button id="searchButton" class="btn btn-primary">๊ฒ€์ƒ‰ํ•˜๊ธฐ</button>
184
+ </div>
185
+ </div>
186
+ </div>
187
+
188
+ <div class="col-md-7">
189
+ <div class="card">
190
+ <div class="card-header">
191
+ <h5 class="card-title mb-0">๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ</h5>
192
+ </div>
193
+ <div class="card-body">
194
+ <div id="searchResults"></div>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ </div>
199
+
200
+ <div id="searchLoading" class="loading">
201
+ <div class="spinner-border text-primary" role="status">
202
+ <span class="visually-hidden">Loading...</span>
203
+ </div>
204
+ <p class="mt-2">๊ฒ€์ƒ‰ ์ค‘...</p>
205
+ </div>
206
+ </div>
207
+ </div>
208
+ </div>
209
+
210
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
211
+ <script>
212
+ // ์ „์—ญ ๋ณ€์ˆ˜
213
+ let currentModel = null;
214
+ let currentDetectionResults = null;
215
+ let currentImageBase64 = null;
216
+
217
+ // DOM ์š”์†Œ
218
+ const imageUpload = document.getElementById('imageUpload');
219
+ const uploadedImage = document.getElementById('uploadedImage');
220
+ const resultImage = document.getElementById('resultImage');
221
+ const detectionResults = document.getElementById('detectionResults');
222
+ const detectionBoxes = document.getElementById('detectionBoxes');
223
+ const saveToVectorDb = document.getElementById('saveToVectorDb');
224
+ const saveResult = document.getElementById('saveResult');
225
+ const detectLoading = document.getElementById('detectLoading');
226
+
227
+ const searchImageUpload = document.getElementById('searchImageUpload');
228
+ const searchImage = document.getElementById('searchImage');
229
+ const searchByImage = document.getElementById('searchByImage');
230
+ const searchByClass = document.getElementById('searchByClass');
231
+ const imageSearchOptions = document.getElementById('imageSearchOptions');
232
+ const classSearchOptions = document.getElementById('classSearchOptions');
233
+ const classInput = document.getElementById('classInput');
234
+ const resultCount = document.getElementById('resultCount');
235
+ const searchButton = document.getElementById('searchButton');
236
+ const searchResults = document.getElementById('searchResults');
237
+ const searchLoading = document.getElementById('searchLoading');
238
+
239
+ // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
240
+ document.getElementById('detectYolo').addEventListener('click', () => detectObjects('yolo'));
241
+ document.getElementById('detectDetr').addEventListener('click', () => detectObjects('detr'));
242
+ document.getElementById('detectVit').addEventListener('click', () => detectObjects('vit'));
243
+ saveToVectorDb.addEventListener('click', saveToVectorDatabase);
244
+ searchButton.addEventListener('click', searchVectorDatabase);
245
+
246
+ // ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ
247
+ imageUpload.addEventListener('change', function(e) {
248
+ if (e.target.files && e.target.files[0]) {
249
+ const reader = new FileReader();
250
+
251
+ reader.onload = function(e) {
252
+ uploadedImage.src = e.target.result;
253
+ uploadedImage.style.display = 'block';
254
+ currentImageBase64 = e.target.result.split(',')[1];
255
+
256
+ // ๊ฒฐ๊ณผ ์ดˆ๊ธฐํ™”
257
+ resultImage.style.display = 'none';
258
+ detectionResults.value = '';
259
+ detectionBoxes.innerHTML = '';
260
+ saveToVectorDb.disabled = true;
261
+ saveResult.innerHTML = '';
262
+ };
263
+
264
+ reader.readAsDataURL(e.target.files[0]);
265
+ }
266
+ });
267
+
268
+ searchImageUpload.addEventListener('change', function(e) {
269
+ if (e.target.files && e.target.files[0]) {
270
+ const reader = new FileReader();
271
+
272
+ reader.onload = function(e) {
273
+ searchImage.src = e.target.result;
274
+ searchImage.style.display = 'block';
275
+ };
276
+
277
+ reader.readAsDataURL(e.target.files[0]);
278
+ }
279
+ });
280
+
281
+ // ๊ฒ€์ƒ‰ ๋ฐฉ๋ฒ• ๋ณ€๊ฒฝ ์ฒ˜๋ฆฌ
282
+ searchByImage.addEventListener('change', function() {
283
+ if (this.checked) {
284
+ imageSearchOptions.style.display = 'block';
285
+ classSearchOptions.style.display = 'none';
286
+ }
287
+ });
288
+
289
+ searchByClass.addEventListener('change', function() {
290
+ if (this.checked) {
291
+ imageSearchOptions.style.display = 'none';
292
+ classSearchOptions.style.display = 'block';
293
+ }
294
+ });
295
+
296
+ // ๊ฐ์ฒด ์ธ์‹ ํ•จ์ˆ˜
297
+ async function detectObjects(model) {
298
+ if (!currentImageBase64) {
299
+ alert('๋จผ์ € ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•ด์ฃผ์„ธ์š”.');
300
+ return;
301
+ }
302
+
303
+ currentModel = model;
304
+ detectLoading.style.display = 'block';
305
+
306
+ try {
307
+ const response = await fetch(`/api/detect?model=${model}`, {
308
+ method: 'POST',
309
+ headers: {
310
+ 'Content-Type': 'application/json',
311
+ },
312
+ body: JSON.stringify({
313
+ image: currentImageBase64
314
+ })
315
+ });
316
+
317
+ const result = await response.json();
318
+
319
+ if (result.error) {
320
+ throw new Error(result.error);
321
+ }
322
+
323
+ // ๊ฒฐ๊ณผ ํ‘œ์‹œ
324
+ if (model === 'vit') {
325
+ // ViT๋Š” ๋ถ„๋ฅ˜๋งŒ ์ œ๊ณต
326
+ resultImage.style.display = 'none';
327
+ detectionBoxes.innerHTML = '';
328
+ detectionResults.value = result.classifications.map(c => `${c.label}: ${c.score.toFixed(2)}`).join('\n');
329
+
330
+ currentDetectionResults = {
331
+ model: 'vit',
332
+ classifications: result.classifications
333
+ };
334
+ } else {
335
+ // YOLO, DETR์€ ๊ฐ์ฒด ์ธ์‹ ๊ฒฐ๊ณผ ์ œ๊ณต
336
+ resultImage.src = `data:image/jpeg;base64,${result.image}`;
337
+ resultImage.style.display = 'block';
338
+
339
+ detectionResults.value = result.objects.map(obj =>
340
+ `${obj.class}: ${obj.confidence.toFixed(2)} at [${obj.bbox.join(', ')}]`
341
+ ).join('\n');
342
+
343
+ // ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ํ‘œ์‹œ
344
+ displayBoundingBoxes(result.objects);
345
+
346
+ currentDetectionResults = {
347
+ model: model,
348
+ objects: result.objects
349
+ };
350
+ }
351
+
352
+ saveToVectorDb.disabled = false;
353
+
354
+ } catch (error) {
355
+ console.error('Error:', error);
356
+ detectionResults.value = `์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error.message}`;
357
+ } finally {
358
+ detectLoading.style.display = 'none';
359
+ }
360
+ }
361
+
362
+ // ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ํ‘œ์‹œ ํ•จ์ˆ˜
363
+ function displayBoundingBoxes(objects) {
364
+ detectionBoxes.innerHTML = '';
365
+
366
+ const imgWidth = resultImage.clientWidth;
367
+ const imgHeight = resultImage.clientHeight;
368
+
369
+ objects.forEach(obj => {
370
+ const [x1, y1, x2, y2] = obj.bbox;
371
+
372
+ const box = document.createElement('div');
373
+ box.className = 'detection-box';
374
+ box.style.left = `${x1 / resultImage.naturalWidth * 100}%`;
375
+ box.style.top = `${y1 / resultImage.naturalHeight * 100}%`;
376
+ box.style.width = `${(x2 - x1) / resultImage.naturalWidth * 100}%`;
377
+ box.style.height = `${(y2 - y1) / resultImage.naturalHeight * 100}%`;
378
+
379
+ // ํด๋ž˜์Šค์— ๋”ฐ๋ผ ์ƒ‰์ƒ ์ง€์ •
380
+ const colors = {
381
+ 'person': 'red',
382
+ 'car': 'blue',
383
+ 'dog': 'green',
384
+ 'cat': 'purple'
385
+ };
386
+
387
+ box.style.borderColor = colors[obj.class] || 'yellow';
388
+
389
+ // ๋ผ๋ฒจ ์ถ”๊ฐ€
390
+ const label = document.createElement('div');
391
+ label.style.position = 'absolute';
392
+ label.style.top = '-20px';
393
+ label.style.left = '0';
394
+ label.style.backgroundColor = box.style.borderColor;
395
+ label.style.color = 'white';
396
+ label.style.padding = '2px 5px';
397
+ label.style.borderRadius = '3px';
398
+ label.style.fontSize = '12px';
399
+ label.textContent = `${obj.class} ${obj.confidence.toFixed(2)}`;
400
+
401
+ box.appendChild(label);
402
+ detectionBoxes.appendChild(box);
403
+ });
404
+ }
405
+
406
+ // ๋ฒกํ„ฐ DB์— ์ €์žฅ ํ•จ์ˆ˜
407
+ async function saveToVectorDatabase() {
408
+ if (!currentDetectionResults) {
409
+ alert('๋จผ์ € ๊ฐ์ฒด๋ฅผ ์ธ์‹ํ•ด์ฃผ์„ธ์š”.');
410
+ return;
411
+ }
412
+
413
+ saveToVectorDb.disabled = true;
414
+ detectLoading.style.display = 'block';
415
+ saveResult.innerHTML = '';
416
+
417
+ try {
418
+ let response;
419
+
420
+ if (currentDetectionResults.model === 'vit') {
421
+ // ViT ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ ์ €์žฅ (์ด๋ฏธ์ง€ ์ „์ฒด ์ €์žฅ)
422
+ response = await fetch('/api/add-image', {
423
+ method: 'POST',
424
+ headers: {
425
+ 'Content-Type': 'application/json',
426
+ },
427
+ body: JSON.stringify({
428
+ image: currentImageBase64,
429
+ metadata: {
430
+ model: 'vit',
431
+ classifications: currentDetectionResults.classifications
432
+ }
433
+ })
434
+ });
435
+ } else {
436
+ // YOLO, DETR ๊ฐ์ฒด ์ธ์‹ ๊ฒฐ๊ณผ ์ €์žฅ
437
+ response = await fetch('/api/add-detected-objects', {
438
+ method: 'POST',
439
+ headers: {
440
+ 'Content-Type': 'application/json',
441
+ },
442
+ body: JSON.stringify({
443
+ image: currentImageBase64,
444
+ objects: currentDetectionResults.objects,
445
+ image_id: generateUUID()
446
+ })
447
+ });
448
+ }
449
+
450
+ const result = await response.json();
451
+
452
+ if (result.error) {
453
+ throw new Error(result.error);
454
+ }
455
+
456
+ // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
457
+ if (currentDetectionResults.model === 'vit') {
458
+ saveResult.innerHTML = `<div class="alert alert-success">์ด๋ฏธ์ง€์™€ ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ๊ฐ€ ๋ฒกํ„ฐ DB์— ์„ฑ๊ณต์ ์œผ๋กœ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.</div>`;
459
+ } else {
460
+ saveResult.innerHTML = `<div class="alert alert-success">${result.object_ids.length}๊ฐœ์˜ ๊ฐ์ฒด๊ฐ€ ๋ฒกํ„ฐ DB์— ์„ฑ๊ณต์ ์œผ๋กœ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.</div>`;
461
+ }
462
+
463
+ } catch (error) {
464
+ console.error('Error:', error);
465
+ saveResult.innerHTML = `<div class="alert alert-danger">์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error.message}</div>`;
466
+ } finally {
467
+ detectLoading.style.display = 'none';
468
+ saveToVectorDb.disabled = false;
469
+ }
470
+ }
471
+
472
+ // ๋ฒกํ„ฐ DB ๊ฒ€์ƒ‰ ํ•จ์ˆ˜
473
+ async function searchVectorDatabase() {
474
+ searchLoading.style.display = 'block';
475
+ searchResults.innerHTML = '';
476
+
477
+ try {
478
+ let response;
479
+ const limit = parseInt(resultCount.value);
480
+
481
+ if (searchByImage.checked) {
482
+ // ์ด๋ฏธ์ง€๋กœ ๊ฒ€์ƒ‰
483
+ if (!searchImage.src || searchImage.src === '') {
484
+ throw new Error('๊ฒ€์ƒ‰ํ•  ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•ด์ฃผ์„ธ์š”.');
485
+ }
486
+
487
+ const imageBase64 = searchImage.src.split(',')[1];
488
+
489
+ response = await fetch('/api/search-similar-objects', {
490
+ method: 'POST',
491
+ headers: {
492
+ 'Content-Type': 'application/json',
493
+ },
494
+ body: JSON.stringify({
495
+ image: imageBase64,
496
+ n_results: limit
497
+ })
498
+ });
499
+ } else {
500
+ // ํด๋ž˜์Šค๋กœ ๊ฒ€์ƒ‰
501
+ if (!classInput.value.trim()) {
502
+ throw new Error('๊ฒ€์ƒ‰ํ•  ํด๋ž˜์Šค ์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.');
503
+ }
504
+
505
+ response = await fetch('/api/search-similar-objects', {
506
+ method: 'POST',
507
+ headers: {
508
+ 'Content-Type': 'application/json',
509
+ },
510
+ body: JSON.stringify({
511
+ class_name: classInput.value.trim(),
512
+ n_results: limit
513
+ })
514
+ });
515
+ }
516
+
517
+ const result = await response.json();
518
+
519
+ if (result.error) {
520
+ throw new Error(result.error);
521
+ }
522
+
523
+ // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ‘œ์‹œ
524
+ displaySearchResults(result);
525
+
526
+ } catch (error) {
527
+ console.error('Error:', error);
528
+ searchResults.innerHTML = `<div class="alert alert-danger">์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error.message}</div>`;
529
+ } finally {
530
+ searchLoading.style.display = 'none';
531
+ }
532
+ }
533
+
534
+ // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ‘œ์‹œ ํ•จ์ˆ˜
535
+ function displaySearchResults(results) {
536
+ if (!results || results.length === 0) {
537
+ searchResults.innerHTML = '<div class="alert alert-info">๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</div>';
538
+ return;
539
+ }
540
+
541
+ let html = '';
542
+
543
+ results.forEach((item, index) => {
544
+ const similarity = (1 - item.distance) * 100;
545
+
546
+ html += `
547
+ <div class="search-result-item">
548
+ <div class="d-flex align-items-center mb-2">
549
+ <h5 class="mb-0 me-2">๊ฒฐ๊ณผ #${index + 1}</h5>
550
+ <span class="badge bg-primary similarity-badge">์œ ์‚ฌ๋„: ${similarity.toFixed(2)}%</span>
551
+ </div>
552
+ <div class="row">
553
+ <div class="col-md-6">
554
+ <img src="data:image/jpeg;base64,${item.image}" class="img-fluid mb-2" alt="Result Image">
555
+ </div>
556
+ <div class="col-md-6">
557
+ <p><strong>ํด๋ž˜์Šค:</strong> ${item.metadata.class || '์ •๋ณด ์—†์Œ'}</p>
558
+ <p><strong>์‹ ๋ขฐ๋„:</strong> ${item.metadata.confidence ? (item.metadata.confidence * 100).toFixed(2) + '%' : '์ •๋ณด ์—†์Œ'}</p>
559
+ <p><strong>์›๋ณธ ์ด๋ฏธ์ง€ ID:</strong> ${item.metadata.image_id || '์ •๋ณด ์—†์Œ'}</p>
560
+ <p><strong>๊ฐ์ฒด ID:</strong> ${item.id}</p>
561
+ </div>
562
+ </div>
563
+ </div>
564
+ `;
565
+ });
566
+
567
+ searchResults.innerHTML = html;
568
+ }
569
+
570
+ // UUID ์ƒ์„ฑ ํ•จ์ˆ˜
571
+ function generateUUID() {
572
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
573
+ const r = Math.random() * 16 | 0;
574
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
575
+ return v.toString(16);
576
+ });
577
+ }
578
+ </script>
579
+ </body>
580
+ </html>