calibrate-suite 0.1.0__py3-none-any.whl

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 (47) hide show
  1. calibrate_suite-0.1.0.dist-info/METADATA +761 -0
  2. calibrate_suite-0.1.0.dist-info/RECORD +47 -0
  3. calibrate_suite-0.1.0.dist-info/WHEEL +5 -0
  4. calibrate_suite-0.1.0.dist-info/entry_points.txt +3 -0
  5. calibrate_suite-0.1.0.dist-info/licenses/LICENSE +201 -0
  6. calibrate_suite-0.1.0.dist-info/top_level.txt +4 -0
  7. fleet_server/__init__.py +32 -0
  8. fleet_server/app.py +377 -0
  9. fleet_server/config.py +91 -0
  10. fleet_server/templates/error.html +57 -0
  11. fleet_server/templates/index.html +137 -0
  12. fleet_server/templates/viewer.html +490 -0
  13. fleet_server/utils.py +178 -0
  14. gui/__init__.py +2 -0
  15. gui/assets/2d-or-3d-fleet-upload.png +0 -0
  16. gui/assets/2d_3d_overlay_output.jpg +0 -0
  17. gui/assets/3d-or-2d-overlay_page.png +0 -0
  18. gui/assets/3d-or-2d-record-page.png +0 -0
  19. gui/assets/3d_3d_overlay_output.png +0 -0
  20. gui/assets/3d_or_2d_calibrate-page.png +0 -0
  21. gui/assets/GUI_homepage.png +0 -0
  22. gui/assets/hardware_setup.jpeg +0 -0
  23. gui/assets/single_lidar_calibrate_page.png +0 -0
  24. gui/assets/single_lidar_output.png +0 -0
  25. gui/assets/single_lidar_record_page.png +0 -0
  26. gui/assets/virya.jpg +0 -0
  27. gui/main.py +23 -0
  28. gui/widgets/calibrator_widget.py +977 -0
  29. gui/widgets/extractor_widget.py +561 -0
  30. gui/widgets/home_widget.py +117 -0
  31. gui/widgets/recorder_widget.py +127 -0
  32. gui/widgets/single_lidar_widget.py +673 -0
  33. gui/widgets/three_d_calib_widget.py +87 -0
  34. gui/widgets/two_d_calib_widget.py +86 -0
  35. gui/widgets/uploader_widget.py +151 -0
  36. gui/widgets/validator_widget.py +614 -0
  37. gui/windows/main_window.py +56 -0
  38. gui/windows/main_window_ui.py +65 -0
  39. rviz_configs/2D-3D.rviz +183 -0
  40. rviz_configs/3D-3D.rviz +184 -0
  41. rviz_configs/default_calib.rviz +167 -0
  42. utils/__init__.py +13 -0
  43. utils/calibration_common.py +23 -0
  44. utils/cli_calibrate.py +53 -0
  45. utils/cli_fleet_server.py +64 -0
  46. utils/data_extractor_common.py +87 -0
  47. utils/gui_helpers.py +25 -0
@@ -0,0 +1,490 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <title>3D Calibration Viewer</title>
7
+ <style>
8
+ body {
9
+ margin: 0;
10
+ overflow: hidden;
11
+ background-color: #000;
12
+ color: white;
13
+ user-select: none;
14
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
15
+ }
16
+
17
+ #info {
18
+ position: absolute;
19
+ top: 10px;
20
+ left: 10px;
21
+ background: rgba(0, 0, 0, 0.7);
22
+ padding: 15px;
23
+ border-radius: 5px;
24
+ pointer-events: none;
25
+ font-family: monospace;
26
+ font-size: 0.9em;
27
+ z-index: 10;
28
+ }
29
+
30
+ #controls {
31
+ position: absolute;
32
+ top: 10px;
33
+ right: 10px;
34
+ background: rgba(30, 30, 30, 0.9);
35
+ padding: 15px;
36
+ border-radius: 5px;
37
+ max-width: 350px;
38
+ max-height: 85vh;
39
+ overflow-y: auto;
40
+ z-index: 20;
41
+ }
42
+
43
+ .control-section {
44
+ margin-bottom: 15px;
45
+ padding-bottom: 15px;
46
+ border-bottom: 1px solid #444;
47
+ }
48
+
49
+ .control-section:last-child {
50
+ border-bottom: none;
51
+ margin-bottom: 0;
52
+ padding-bottom: 0;
53
+ }
54
+
55
+ .section-title {
56
+ font-weight: bold;
57
+ color: #2196F3;
58
+ margin-bottom: 10px;
59
+ font-size: 0.95em;
60
+ }
61
+
62
+ button {
63
+ background-color: #2196F3;
64
+ color: white;
65
+ border: none;
66
+ padding: 8px 12px;
67
+ border-radius: 4px;
68
+ cursor: pointer;
69
+ font-size: 0.9em;
70
+ margin-top: 5px;
71
+ width: 100%;
72
+ transition: background-color 0.2s;
73
+ }
74
+
75
+ button:hover {
76
+ background-color: #1976D2;
77
+ }
78
+
79
+ button:disabled {
80
+ background-color: #666;
81
+ cursor: not-allowed;
82
+ }
83
+
84
+ #btnStartTest {
85
+ background-color: #4CAF50;
86
+ }
87
+
88
+ #btnStartTest:hover:not(:disabled) {
89
+ background-color: #45a049;
90
+ }
91
+
92
+ #fileInput {
93
+ display: none;
94
+ }
95
+
96
+ .file-status {
97
+ font-size: 0.85em;
98
+ padding: 10px;
99
+ background: rgba(255, 255, 255, 0.05);
100
+ border-radius: 4px;
101
+ margin-top: 10px;
102
+ }
103
+
104
+ .status-row {
105
+ display: flex;
106
+ justify-content: space-between;
107
+ align-items: center;
108
+ padding: 5px 0;
109
+ border-bottom: 1px solid #333;
110
+ }
111
+
112
+ .status-row:last-child {
113
+ border-bottom: none;
114
+ }
115
+
116
+ .status-label {
117
+ font-weight: 500;
118
+ }
119
+
120
+ .status-badge {
121
+ display: inline-block;
122
+ padding: 2px 8px;
123
+ border-radius: 3px;
124
+ font-size: 0.8em;
125
+ font-weight: bold;
126
+ }
127
+
128
+ .badge-ok {
129
+ background-color: #4CAF50;
130
+ color: white;
131
+ }
132
+
133
+ .badge-pending {
134
+ background-color: #FF9800;
135
+ color: white;
136
+ }
137
+
138
+ .hints {
139
+ font-size: 0.8em;
140
+ color: #aaa;
141
+ margin-top: 10px;
142
+ line-height: 1.5;
143
+ }
144
+
145
+ .spinner {
146
+ display: inline-block;
147
+ width: 12px;
148
+ height: 12px;
149
+ border: 2px solid #f3f3f3;
150
+ border-top: 2px solid #2196F3;
151
+ border-radius: 50%;
152
+ animation: spin 1s linear infinite;
153
+ margin-right: 5px;
154
+ }
155
+
156
+ @keyframes spin {
157
+ 0% { transform: rotate(0deg); }
158
+ 100% { transform: rotate(360deg); }
159
+ }
160
+ </style>
161
+ <!-- Three.js CDN -->
162
+ <script type="importmap">
163
+ {
164
+ "imports": {
165
+ "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
166
+ "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
167
+ }
168
+ }
169
+ </script>
170
+ </head>
171
+
172
+ <body>
173
+ <div id="info">
174
+ <strong>Calibration Viewer</strong><br>
175
+ Robot: {{ calib['robot_id'] }}<br>
176
+ Time: {{ calib['timestamp'] | format_timestamp }}<br>
177
+ Score: {{ calib['score'] }} m<br>
178
+ <span id="loadStatus" style="font-size: 0.8em; color: #aaa;"></span>
179
+ </div>
180
+
181
+ <div id="controls">
182
+ <!-- Visualization Controls -->
183
+ <div class="control-section">
184
+ <div class="section-title">🎮 Visualization</div>
185
+ <button id="btnReset">Reset View</button>
186
+ <div class="hints">
187
+ LMB: Rotate | RMB: Pan | Scroll: Zoom
188
+ </div>
189
+ </div>
190
+
191
+ <!-- PCD File Management -->
192
+ <div class="control-section">
193
+ <div class="section-title">📁 Point Cloud Upload</div>
194
+ <button id="btnBrowsePCD">📂 Select PCD File</button>
195
+ <input type="file" id="fileInput" accept=".pcd" />
196
+ <button id="btnUploadPCD" style="background-color: #FF9800;" disabled>☁️ Upload PCD</button>
197
+
198
+ <div class="file-status">
199
+ <div class="status-row">
200
+ <span class="status-label">Selected:</span>
201
+ <span id="selectedFile" style="color: #aaa; font-size: 0.9em;">None</span>
202
+ </div>
203
+ <div class="status-row" style="margin-top: 5px;">
204
+ <span class="status-label">Size:</span>
205
+ <span id="selectedSize" style="color: #aaa;">-</span>
206
+ </div>
207
+ </div>
208
+ </div>
209
+
210
+ <!-- File Status Table -->
211
+ <div class="control-section">
212
+ <div class="section-title">📊 Calibration Files</div>
213
+ <div class="file-status">
214
+ <div class="status-row">
215
+ <span class="status-label">Extrinsic</span>
216
+ <span class="status-badge badge-ok">✓ Uploaded</span>
217
+ </div>
218
+ <div class="status-row">
219
+ <span class="status-label">Point Cloud</span>
220
+ <span id="pcdStatus" class="status-badge badge-pending">⏳ Pending</span>
221
+ </div>
222
+ </div>
223
+ </div>
224
+
225
+ <!-- Start Testing -->
226
+ <div class="control-section">
227
+ <button id="btnStartTest" disabled>▶️ Start Testing</button>
228
+ <div class="hints">
229
+ Upload PCD file and click to begin validation with extrinsic calibration.
230
+ </div>
231
+ </div>
232
+
233
+ <!-- Upload Progress -->
234
+ <div id="uploadProgress" class="control-section" style="display: none;">
235
+ <span class="spinner"></span>
236
+ <span id="progressText">Uploading...</span>
237
+ </div>
238
+ </div>
239
+
240
+ <script type="module">
241
+ import * as THREE from 'three';
242
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
243
+ import { PCDLoader } from 'three/addons/loaders/PCDLoader.js';
244
+
245
+ let camera, scene, renderer, controls;
246
+ let selectedFile = null;
247
+ let currentPCDUrl = "{{ file_url }}";
248
+ const robotId = "{{ calib['robot_id'] }}";
249
+
250
+ init();
251
+ setupEventListeners();
252
+ animate();
253
+
254
+ // ============ Initialization ============
255
+ function init() {
256
+ // Renderer
257
+ renderer = new THREE.WebGLRenderer({ antialias: true });
258
+ renderer.setPixelRatio(window.devicePixelRatio);
259
+ renderer.setSize(window.innerWidth, window.innerHeight);
260
+ document.body.appendChild(renderer.domElement);
261
+
262
+ // Scene
263
+ scene = new THREE.Scene();
264
+ scene.background = new THREE.Color(0x000000);
265
+
266
+ // Camera
267
+ camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
268
+ camera.position.set(0, 5, 10);
269
+ camera.up.set(0, 0, 1);
270
+
271
+ // Controls
272
+ controls = new OrbitControls(camera, renderer.domElement);
273
+ controls.minDistance = 0.5;
274
+ controls.maxDistance = 50;
275
+ controls.target.set(0, 0, 0);
276
+ controls.update();
277
+
278
+ // Lights
279
+ scene.add(new THREE.AmbientLight(0x555555));
280
+ const dirLight = new THREE.DirectionalLight(0xffffff, 3);
281
+ dirLight.position.set(1, 1, 1);
282
+ scene.add(dirLight);
283
+
284
+ // Grid
285
+ const gridHelper = new THREE.GridHelper(20, 20, 0x444444, 0x222222);
286
+ gridHelper.rotation.x = Math.PI / 2;
287
+ scene.add(gridHelper);
288
+
289
+ // Axes
290
+ const axesHelper = new THREE.AxesHelper(1);
291
+ scene.add(axesHelper);
292
+
293
+ // Load initial PCD
294
+ loadPCD(currentPCDUrl);
295
+
296
+ // Events
297
+ window.addEventListener('resize', onWindowResize);
298
+ }
299
+
300
+ // ============ Event Listeners ============
301
+ function setupEventListeners() {
302
+ document.getElementById('btnReset').onclick = () => controls.reset();
303
+ document.getElementById('btnBrowsePCD').onclick = () => document.getElementById('fileInput').click();
304
+ document.getElementById('fileInput').onchange = handleFileSelect;
305
+ document.getElementById('btnUploadPCD').onclick = uploadPCDFile;
306
+ document.getElementById('btnStartTest').onclick = startTesting;
307
+ }
308
+
309
+ function handleFileSelect(event) {
310
+ const file = event.target.files[0];
311
+ if (!file) return;
312
+
313
+ if (!file.name.toLowerCase().endsWith('.pcd')) {
314
+ alert('Please select a .pcd file');
315
+ return;
316
+ }
317
+
318
+ selectedFile = file;
319
+ const fileName = file.name.length > 30 ? file.name.substring(0, 27) + '...' : file.name;
320
+ const fileSize = formatFileSize(file.size);
321
+
322
+ document.getElementById('selectedFile').textContent = fileName;
323
+ document.getElementById('selectedSize').textContent = fileSize;
324
+ document.getElementById('btnUploadPCD').disabled = false;
325
+ }
326
+
327
+ async function uploadPCDFile() {
328
+ if (!selectedFile) {
329
+ alert('Please select a file first');
330
+ return;
331
+ }
332
+
333
+ const formData = new FormData();
334
+ formData.append('artifacts', selectedFile);
335
+ formData.append('metadata', JSON.stringify({
336
+ robot_id: robotId,
337
+ score: 0.0
338
+ }));
339
+
340
+ showProgress(true, 'Uploading...');
341
+ document.getElementById('btnUploadPCD').disabled = true;
342
+
343
+ try {
344
+ const response = await fetch('/api/upload', {
345
+ method: 'POST',
346
+ body: formData
347
+ });
348
+
349
+ const result = await response.json();
350
+
351
+ if (response.ok) {
352
+ // Update the viewer with new PCD
353
+ const newFileUrl = `/files/${result.filename}`;
354
+ currentPCDUrl = newFileUrl;
355
+
356
+ // Clear scene and reload PCD
357
+ scene.children = scene.children.filter(child =>
358
+ !(child instanceof THREE.Points)
359
+ );
360
+
361
+ loadPCD(newFileUrl);
362
+
363
+ // Update status
364
+ updatePCDStatus(true);
365
+ document.getElementById('selectedFile').textContent = 'Uploaded ✓';
366
+ document.getElementById('selectedSize').textContent = result.file_size + ' bytes';
367
+
368
+ showProgress(false);
369
+ } else {
370
+ alert('Upload failed: ' + (result.error || 'Unknown error'));
371
+ showProgress(false);
372
+ document.getElementById('btnUploadPCD').disabled = false;
373
+ }
374
+ } catch (error) {
375
+ console.error('Upload error:', error);
376
+ alert('Upload error: ' + error.message);
377
+ showProgress(false);
378
+ document.getElementById('btnUploadPCD').disabled = false;
379
+ }
380
+ }
381
+
382
+ function loadPCD(fileUrl) {
383
+ if (!fileUrl || !fileUrl.toLowerCase().endsWith('.pcd')) {
384
+ showNonPCDMessage();
385
+ return;
386
+ }
387
+
388
+ const loader = new PCDLoader();
389
+ console.log('Loading PCD from:', fileUrl);
390
+ document.getElementById('loadStatus').textContent = 'Loading...';
391
+
392
+ loader.load(
393
+ fileUrl,
394
+ function (points) {
395
+ // Success
396
+ points.geometry.center();
397
+ points.geometry.rotateX(-Math.PI / 2);
398
+ points.material.color.setHex(0x00ff00);
399
+ points.material.size = 0.02;
400
+ scene.add(points);
401
+ console.log('PCD Loaded successfully');
402
+ document.getElementById('loadStatus').textContent = 'Loaded ✓';
403
+ },
404
+ function (progress) {
405
+ // Progress
406
+ const percent = (progress.loaded / progress.total * 100).toFixed(0);
407
+ document.getElementById('loadStatus').textContent = `Loading ${percent}%...`;
408
+ },
409
+ function (error) {
410
+ // Error
411
+ console.error('PCD loading error:', error);
412
+ document.getElementById('loadStatus').textContent = 'Load error ✗';
413
+ alert('Error loading PCD file. Is it a valid point cloud?');
414
+ }
415
+ );
416
+ }
417
+
418
+ function showNonPCDMessage() {
419
+ const div = document.createElement('div');
420
+ div.style.position = 'absolute';
421
+ div.style.top = '50%';
422
+ div.style.width = '100%';
423
+ div.style.textAlign = 'center';
424
+ div.style.color = '#ff8800';
425
+ div.style.zIndex = '5';
426
+ div.innerHTML = '<h3>Visualization only available for .pcd files</h3><p>Use the file browser to upload a point cloud.</p>';
427
+ document.body.appendChild(div);
428
+ }
429
+
430
+ function updatePCDStatus(isUploaded) {
431
+ const badge = document.getElementById('pcdStatus');
432
+ const testBtn = document.getElementById('btnStartTest');
433
+
434
+ if (isUploaded) {
435
+ badge.textContent = '✓ Uploaded';
436
+ badge.className = 'status-badge badge-ok';
437
+ testBtn.disabled = false;
438
+ } else {
439
+ badge.textContent = '⏳ Pending';
440
+ badge.className = 'status-badge badge-pending';
441
+ testBtn.disabled = true;
442
+ }
443
+ }
444
+
445
+ function startTesting() {
446
+ const message = `Ready to test calibration!\n\n` +
447
+ `Robot: ${robotId}\n` +
448
+ `Extrinsic: ✓ Uploaded\n` +
449
+ `Point Cloud: ✓ Uploaded\n\n` +
450
+ `Next steps:\n` +
451
+ `1. Review the point cloud in the 3D viewer\n` +
452
+ `2. Check alignment quality\n` +
453
+ `3. Download files for deployment if satisfied`;
454
+ alert(message);
455
+ }
456
+
457
+ function showProgress(show, text = 'Uploading...') {
458
+ const progressDiv = document.getElementById('uploadProgress');
459
+ if (show) {
460
+ document.getElementById('progressText').textContent = text;
461
+ progressDiv.style.display = 'block';
462
+ } else {
463
+ progressDiv.style.display = 'none';
464
+ }
465
+ }
466
+
467
+ function formatFileSize(bytes) {
468
+ if (bytes === 0) return '0 Bytes';
469
+ const k = 1024;
470
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
471
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
472
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
473
+ }
474
+
475
+ // ============ Rendering ============
476
+ function onWindowResize() {
477
+ camera.aspect = window.innerWidth / window.innerHeight;
478
+ camera.updateProjectionMatrix();
479
+ renderer.setSize(window.innerWidth, window.innerHeight);
480
+ }
481
+
482
+ function animate() {
483
+ requestAnimationFrame(animate);
484
+ controls.update();
485
+ renderer.render(scene, camera);
486
+ }
487
+ </script>
488
+ </body>
489
+
490
+ </html>
fleet_server/utils.py ADDED
@@ -0,0 +1,178 @@
1
+ """Fleet Server Utility Functions
2
+
3
+ Provides input validation, file handling, and logging utilities.
4
+ """
5
+
6
+ import os
7
+ import re
8
+ import logging
9
+ from pathlib import Path
10
+ from .config import Config
11
+
12
+ # Configure logging
13
+ def setup_logging():
14
+ """Setup logging for fleet server"""
15
+ log_file = Config.LOG_FILE
16
+ log_level = getattr(logging, Config.LOG_LEVEL, logging.INFO)
17
+
18
+ logger = logging.getLogger('fleet_server')
19
+ logger.setLevel(log_level)
20
+
21
+ # Ensure log directory exists
22
+ log_dir = os.path.dirname(log_file)
23
+ if log_dir:
24
+ os.makedirs(log_dir, exist_ok=True)
25
+
26
+ # File handler
27
+ file_handler = logging.FileHandler(log_file)
28
+ file_handler.setLevel(log_level)
29
+
30
+ # Console handler
31
+ console_handler = logging.StreamHandler()
32
+ console_handler.setLevel(log_level)
33
+
34
+ # Formatter
35
+ formatter = logging.Formatter(
36
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
37
+ )
38
+ file_handler.setFormatter(formatter)
39
+ console_handler.setFormatter(formatter)
40
+
41
+ logger.addHandler(file_handler)
42
+ logger.addHandler(console_handler)
43
+
44
+ return logger
45
+
46
+ logger = setup_logging()
47
+
48
+ def sanitize_filename(filename):
49
+ """
50
+ Sanitize filename to prevent directory traversal attacks.
51
+
52
+ Args:
53
+ filename: Original filename from upload
54
+
55
+ Returns:
56
+ Safe filename string
57
+ """
58
+ # Remove path components
59
+ filename = os.path.basename(filename)
60
+
61
+ # Remove potentially dangerous characters
62
+ # Keep only alphanumeric, dash, underscore, dot
63
+ filename = re.sub(r'[^\w\-\.]', '_', filename)
64
+
65
+ # Remove multiple dots (prevents .pcd.exe attacks)
66
+ filename = re.sub(r'\.{2,}', '.', filename)
67
+
68
+ # Limit length
69
+ name, ext = os.path.splitext(filename)
70
+ if len(name) > 100:
71
+ name = name[:100]
72
+ filename = name + ext
73
+
74
+ return filename
75
+
76
+ def validate_file_extension(filename, allowed=None):
77
+ """
78
+ Validate file extension against allowed list.
79
+
80
+ Args:
81
+ filename: Filename to validate
82
+ allowed: Set of allowed extensions (defaults to Config.ALLOWED_EXTENSIONS)
83
+
84
+ Returns:
85
+ True if extension is allowed, False otherwise
86
+ """
87
+ if allowed is None:
88
+ allowed = Config.ALLOWED_EXTENSIONS
89
+
90
+ _, ext = os.path.splitext(filename)
91
+ ext = ext.lstrip('.').lower()
92
+
93
+ return ext in allowed
94
+
95
+ def validate_file_size(file_obj, max_size=None):
96
+ """
97
+ Validate file size.
98
+
99
+ Args:
100
+ file_obj: File-like object to validate
101
+ max_size: Maximum size in bytes (defaults to Config.MAX_UPLOAD_SIZE)
102
+
103
+ Returns:
104
+ True if within size limit, False otherwise
105
+ """
106
+ if max_size is None:
107
+ max_size = Config.MAX_UPLOAD_SIZE
108
+
109
+ file_obj.seek(0, os.SEEK_END)
110
+ size = file_obj.tell()
111
+ file_obj.seek(0)
112
+
113
+ return size <= max_size
114
+
115
+ def validate_upload(file_obj, filename):
116
+ """
117
+ Comprehensive validation for file uploads.
118
+
119
+ Args:
120
+ file_obj: File object from request
121
+ filename: Filename from upload
122
+
123
+ Returns:
124
+ Tuple (is_valid, error_message)
125
+ """
126
+ # Check filename
127
+ if not filename or filename == '':
128
+ return False, "No filename provided"
129
+
130
+ # Check extension
131
+ if not validate_file_extension(filename):
132
+ return False, f"File type not allowed. Allowed: {', '.join(Config.ALLOWED_EXTENSIONS)}"
133
+
134
+ # Check file size
135
+ if not validate_file_size(file_obj):
136
+ max_mb = Config.MAX_UPLOAD_SIZE / (1024 * 1024)
137
+ return False, f"File too large (max {max_mb:.0f}MB)"
138
+
139
+ return True, "Valid"
140
+
141
+ def get_safe_path(base_dir, filename):
142
+ """
143
+ Get safe absolute path within base directory.
144
+ Prevents directory traversal attacks.
145
+
146
+ Args:
147
+ base_dir: Base directory (absolute path)
148
+ filename: Requested filename
149
+
150
+ Returns:
151
+ Safe absolute path if within base_dir, None otherwise
152
+ """
153
+ base = Path(base_dir).resolve()
154
+ requested = (base / filename).resolve()
155
+
156
+ # Ensure requested path is within base directory
157
+ try:
158
+ requested.relative_to(base)
159
+ return str(requested)
160
+ except ValueError:
161
+ logger.warning(f"Attempted directory traversal: {requested}")
162
+ return None
163
+
164
+ def format_filesize(size_bytes):
165
+ """
166
+ Format bytes to human-readable size.
167
+
168
+ Args:
169
+ size_bytes: Size in bytes
170
+
171
+ Returns:
172
+ Formatted string (e.g., "1.5 MB")
173
+ """
174
+ for unit in ['B', 'KB', 'MB', 'GB']:
175
+ if size_bytes < 1024.0:
176
+ return f"{size_bytes:.2f} {unit}"
177
+ size_bytes /= 1024.0
178
+ return f"{size_bytes:.2f} TB"
gui/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """GUI Package - CalibStudio Desktop Application"""
2
+ __all__ = ["main"]
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
gui/assets/virya.jpg ADDED
Binary file