rtmlib-ts 0.0.2

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 (150) hide show
  1. package/.gitattributes +1 -0
  2. package/README.md +202 -0
  3. package/dist/core/base.d.ts +20 -0
  4. package/dist/core/base.d.ts.map +1 -0
  5. package/dist/core/base.js +40 -0
  6. package/dist/core/file.d.ts +11 -0
  7. package/dist/core/file.d.ts.map +1 -0
  8. package/dist/core/file.js +111 -0
  9. package/dist/core/modelCache.d.ts +35 -0
  10. package/dist/core/modelCache.d.ts.map +1 -0
  11. package/dist/core/modelCache.js +161 -0
  12. package/dist/core/posePostprocessing.d.ts +12 -0
  13. package/dist/core/posePostprocessing.d.ts.map +1 -0
  14. package/dist/core/posePostprocessing.js +76 -0
  15. package/dist/core/postprocessing.d.ts +10 -0
  16. package/dist/core/postprocessing.d.ts.map +1 -0
  17. package/dist/core/postprocessing.js +70 -0
  18. package/dist/core/preprocessing.d.ts +14 -0
  19. package/dist/core/preprocessing.d.ts.map +1 -0
  20. package/dist/core/preprocessing.js +79 -0
  21. package/dist/index.d.ts +27 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +31 -0
  24. package/dist/models/rtmpose.d.ts +25 -0
  25. package/dist/models/rtmpose.d.ts.map +1 -0
  26. package/dist/models/rtmpose.js +185 -0
  27. package/dist/models/rtmpose3d.d.ts +28 -0
  28. package/dist/models/rtmpose3d.d.ts.map +1 -0
  29. package/dist/models/rtmpose3d.js +184 -0
  30. package/dist/models/yolo12.d.ts +23 -0
  31. package/dist/models/yolo12.d.ts.map +1 -0
  32. package/dist/models/yolo12.js +165 -0
  33. package/dist/models/yolox.d.ts +18 -0
  34. package/dist/models/yolox.d.ts.map +1 -0
  35. package/dist/models/yolox.js +167 -0
  36. package/dist/solution/animalDetector.d.ts +229 -0
  37. package/dist/solution/animalDetector.d.ts.map +1 -0
  38. package/dist/solution/animalDetector.js +663 -0
  39. package/dist/solution/body.d.ts +16 -0
  40. package/dist/solution/body.d.ts.map +1 -0
  41. package/dist/solution/body.js +52 -0
  42. package/dist/solution/bodyWithFeet.d.ts +16 -0
  43. package/dist/solution/bodyWithFeet.d.ts.map +1 -0
  44. package/dist/solution/bodyWithFeet.js +52 -0
  45. package/dist/solution/customDetector.d.ts +137 -0
  46. package/dist/solution/customDetector.d.ts.map +1 -0
  47. package/dist/solution/customDetector.js +342 -0
  48. package/dist/solution/hand.d.ts +14 -0
  49. package/dist/solution/hand.d.ts.map +1 -0
  50. package/dist/solution/hand.js +20 -0
  51. package/dist/solution/index.d.ts +10 -0
  52. package/dist/solution/index.d.ts.map +1 -0
  53. package/dist/solution/index.js +9 -0
  54. package/dist/solution/objectDetector.d.ts +172 -0
  55. package/dist/solution/objectDetector.d.ts.map +1 -0
  56. package/dist/solution/objectDetector.js +606 -0
  57. package/dist/solution/pose3dDetector.d.ts +145 -0
  58. package/dist/solution/pose3dDetector.d.ts.map +1 -0
  59. package/dist/solution/pose3dDetector.js +611 -0
  60. package/dist/solution/poseDetector.d.ts +198 -0
  61. package/dist/solution/poseDetector.d.ts.map +1 -0
  62. package/dist/solution/poseDetector.js +622 -0
  63. package/dist/solution/poseTracker.d.ts +22 -0
  64. package/dist/solution/poseTracker.d.ts.map +1 -0
  65. package/dist/solution/poseTracker.js +106 -0
  66. package/dist/solution/wholebody.d.ts +19 -0
  67. package/dist/solution/wholebody.d.ts.map +1 -0
  68. package/dist/solution/wholebody.js +82 -0
  69. package/dist/solution/wholebody3d.d.ts +22 -0
  70. package/dist/solution/wholebody3d.d.ts.map +1 -0
  71. package/dist/solution/wholebody3d.js +75 -0
  72. package/dist/types/index.d.ts +52 -0
  73. package/dist/types/index.d.ts.map +1 -0
  74. package/dist/types/index.js +5 -0
  75. package/dist/visualization/draw.d.ts +57 -0
  76. package/dist/visualization/draw.d.ts.map +1 -0
  77. package/dist/visualization/draw.js +400 -0
  78. package/dist/visualization/skeleton/coco133.d.ts +350 -0
  79. package/dist/visualization/skeleton/coco133.d.ts.map +1 -0
  80. package/dist/visualization/skeleton/coco133.js +120 -0
  81. package/dist/visualization/skeleton/coco17.d.ts +180 -0
  82. package/dist/visualization/skeleton/coco17.d.ts.map +1 -0
  83. package/dist/visualization/skeleton/coco17.js +48 -0
  84. package/dist/visualization/skeleton/halpe26.d.ts +278 -0
  85. package/dist/visualization/skeleton/halpe26.d.ts.map +1 -0
  86. package/dist/visualization/skeleton/halpe26.js +70 -0
  87. package/dist/visualization/skeleton/hand21.d.ts +196 -0
  88. package/dist/visualization/skeleton/hand21.d.ts.map +1 -0
  89. package/dist/visualization/skeleton/hand21.js +51 -0
  90. package/dist/visualization/skeleton/index.d.ts +10 -0
  91. package/dist/visualization/skeleton/index.d.ts.map +1 -0
  92. package/dist/visualization/skeleton/index.js +9 -0
  93. package/dist/visualization/skeleton/openpose134.d.ts +357 -0
  94. package/dist/visualization/skeleton/openpose134.d.ts.map +1 -0
  95. package/dist/visualization/skeleton/openpose134.js +116 -0
  96. package/dist/visualization/skeleton/openpose18.d.ts +177 -0
  97. package/dist/visualization/skeleton/openpose18.d.ts.map +1 -0
  98. package/dist/visualization/skeleton/openpose18.js +47 -0
  99. package/docs/ANIMAL_DETECTOR.md +450 -0
  100. package/docs/CUSTOM_DETECTOR.md +568 -0
  101. package/docs/OBJECT_DETECTOR.md +373 -0
  102. package/docs/POSE3D_DETECTOR.md +458 -0
  103. package/docs/POSE_DETECTOR.md +442 -0
  104. package/examples/README.md +119 -0
  105. package/examples/index.html +746 -0
  106. package/package.json +51 -0
  107. package/playground/README.md +114 -0
  108. package/playground/app/favicon.ico +0 -0
  109. package/playground/app/globals.css +17 -0
  110. package/playground/app/layout.tsx +19 -0
  111. package/playground/app/page.tsx +1338 -0
  112. package/playground/eslint.config.mjs +18 -0
  113. package/playground/next.config.ts +34 -0
  114. package/playground/package-lock.json +6723 -0
  115. package/playground/package.json +27 -0
  116. package/playground/postcss.config.mjs +7 -0
  117. package/playground/tsconfig.json +34 -0
  118. package/src/core/base.ts +66 -0
  119. package/src/core/file.ts +141 -0
  120. package/src/core/modelCache.ts +189 -0
  121. package/src/core/posePostprocessing.ts +91 -0
  122. package/src/core/postprocessing.ts +93 -0
  123. package/src/core/preprocessing.ts +127 -0
  124. package/src/index.ts +69 -0
  125. package/src/models/rtmpose.ts +265 -0
  126. package/src/models/rtmpose3d.ts +289 -0
  127. package/src/models/yolo12.ts +220 -0
  128. package/src/models/yolox.ts +214 -0
  129. package/src/solution/animalDetector.ts +955 -0
  130. package/src/solution/body.ts +89 -0
  131. package/src/solution/bodyWithFeet.ts +89 -0
  132. package/src/solution/customDetector.ts +474 -0
  133. package/src/solution/hand.ts +52 -0
  134. package/src/solution/index.ts +10 -0
  135. package/src/solution/objectDetector.ts +816 -0
  136. package/src/solution/pose3dDetector.ts +890 -0
  137. package/src/solution/poseDetector.ts +892 -0
  138. package/src/solution/poseTracker.ts +172 -0
  139. package/src/solution/wholebody.ts +130 -0
  140. package/src/solution/wholebody3d.ts +125 -0
  141. package/src/types/index.ts +62 -0
  142. package/src/visualization/draw.ts +543 -0
  143. package/src/visualization/skeleton/coco133.ts +131 -0
  144. package/src/visualization/skeleton/coco17.ts +49 -0
  145. package/src/visualization/skeleton/halpe26.ts +71 -0
  146. package/src/visualization/skeleton/hand21.ts +52 -0
  147. package/src/visualization/skeleton/index.ts +10 -0
  148. package/src/visualization/skeleton/openpose134.ts +125 -0
  149. package/src/visualization/skeleton/openpose18.ts +48 -0
  150. package/tsconfig.json +32 -0
@@ -0,0 +1,1338 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, useRef } from 'react';
4
+ import {
5
+ ObjectDetector,
6
+ PoseDetector,
7
+ Pose3DDetector,
8
+ AnimalDetector,
9
+ VITPOSE_MODELS,
10
+ ANIMAL_CLASSES
11
+ } from '../../dist/index.js';
12
+ import {
13
+ clearModelCache,
14
+ drawDetectionsOnCanvas,
15
+ drawPoseOnCanvas,
16
+ getCacheInfo,
17
+ } from '../../dist/index.js';
18
+
19
+ // Types
20
+ interface Detection {
21
+ bbox: { x1: number; y1: number; x2: number; y2: number; confidence: number };
22
+ className?: string;
23
+ keypoints?: any[];
24
+ keypoints3d?: number[][];
25
+ classId?: number;
26
+ }
27
+
28
+ interface ModelStatus {
29
+ loaded: boolean;
30
+ loading: boolean;
31
+ error?: string;
32
+ }
33
+
34
+ export default function Home() {
35
+ const canvasRef = useRef<HTMLCanvasElement>(null);
36
+ const videoRef = useRef<HTMLVideoElement>(null);
37
+
38
+ const [objectDetector, setObjectDetector] = useState<any>(null);
39
+ const [poseDetector, setPoseDetector] = useState<any>(null);
40
+ const [pose3DDetector, setPose3DDetector] = useState<any>(null);
41
+ const [animalDetector, setAnimalDetector] = useState<any>(null);
42
+
43
+ const [mode, setMode] = useState<'object' | 'pose' | 'pose3d' | 'animal'>('object');
44
+ const [perfMode, setPerfMode] = useState<'performance' | 'balanced' | 'lightweight'>('balanced');
45
+ const [backend, setBackend] = useState<'wasm' | 'webgpu'>(() => {
46
+ // Check if WebGPU is available
47
+ if (typeof navigator !== 'undefined' && (navigator as any).gpu) {
48
+ return 'webgpu';
49
+ }
50
+ // Fallback to WASM if WebGPU is not available
51
+ return 'wasm';
52
+ });
53
+ const [animalPoseModel, setAnimalPoseModel] = useState<'vitpose-s' | 'vitpose-b' | 'vitpose-l'>('vitpose-b');
54
+ const [selectedClasses, setSelectedClasses] = useState<string[]>(['person']);
55
+ const [selectedAnimalClasses, setSelectedAnimalClasses] = useState<string[]>([]);
56
+ const [detections, setDetections] = useState<Detection[]>([]);
57
+ const [stats, setStats] = useState<{ time: number; count: number; detTime?: number; poseTime?: number } | null>(null);
58
+ const [useCamera, setUseCamera] = useState(false);
59
+ const [hasImage, setHasImage] = useState(false);
60
+ const [videoSrc, setVideoSrc] = useState<string | null>(null);
61
+ const [isPlaying, setIsPlaying] = useState(false);
62
+ const [detectionInterval, setDetectionInterval] = useState<NodeJS.Timeout | null>(null);
63
+ const [cacheInfo, setCacheInfo] = useState<{ size: string; cached: number } | null>(null);
64
+ const [showDocs, setShowDocs] = useState(false);
65
+
66
+ // Performance optimization: process every Nth frame
67
+ const [processEveryNFrames, setProcessEveryNFrames] = useState(3);
68
+ const frameCountRef = useRef(0);
69
+
70
+ const [modelStatus, setModelStatus] = useState<Record<string, ModelStatus>>({
71
+ object: { loaded: false, loading: false },
72
+ pose: { loaded: false, loading: false },
73
+ pose3d: { loaded: false, loading: false },
74
+ animal: { loaded: false, loading: false },
75
+ });
76
+
77
+
78
+
79
+ useEffect(() => {
80
+ async function loadCacheInfo() {
81
+ const cacheInfo = await getCacheInfo();
82
+ setCacheInfo({
83
+ size: cacheInfo.totalSizeFormatted,
84
+ cached: cacheInfo.cachedModels.length,
85
+ });
86
+ }
87
+ loadCacheInfo();
88
+ }, []);
89
+
90
+ // Fallback detector initialization
91
+ const initDetectorWithFallback = async (
92
+ mode: 'object' | 'pose' | 'pose3d' | 'animal',
93
+ perfMode: 'performance' | 'balanced' | 'lightweight',
94
+ fallbackBackend: 'wasm'
95
+ ) => {
96
+ try {
97
+ let detector: any;
98
+ const startTime = performance.now();
99
+
100
+ if (mode === 'object') {
101
+ detector = new ObjectDetector({
102
+ classes: selectedClasses,
103
+ mode: perfMode,
104
+ backend: fallbackBackend,
105
+ confidence: 0.3,
106
+ cache: true,
107
+ });
108
+ } else if (mode === 'pose') {
109
+ detector = new PoseDetector({
110
+ detConfidence: 0.5,
111
+ poseConfidence: 0.3,
112
+ backend: fallbackBackend,
113
+ cache: true,
114
+ });
115
+ } else if (mode === 'pose3d') {
116
+ detector = new Pose3DDetector({
117
+ detModel: "./end2end.onnx",
118
+ detConfidence: 0.45,
119
+ poseConfidence: 0.3,
120
+ backend: fallbackBackend,
121
+ cache: true,
122
+ });
123
+ } else if (mode === 'animal') {
124
+ detector = new AnimalDetector({
125
+ classes: selectedAnimalClasses.length > 0 ? selectedAnimalClasses : null,
126
+ poseModelType: animalPoseModel,
127
+ detConfidence: 0.5,
128
+ poseConfidence: 0.3,
129
+ backend: fallbackBackend,
130
+ cache: true,
131
+ });
132
+ }
133
+
134
+ await detector.init();
135
+ const loadTime = Math.round(performance.now() - startTime);
136
+
137
+ if (mode === 'object') setObjectDetector(detector);
138
+ else if (mode === 'pose') setPoseDetector(detector);
139
+ else if (mode === 'pose3d') setPose3DDetector(detector);
140
+ else if (mode === 'animal') setAnimalDetector(detector);
141
+
142
+ setModelStatus(prev => ({
143
+ ...prev,
144
+ [mode]: { loaded: true, loading: false }
145
+ }));
146
+
147
+ console.log(`${mode} detector initialized with WASM fallback in ${loadTime}ms`);
148
+ } catch (error) {
149
+ console.error(`Failed to load ${mode} detector with WASM fallback:`, error);
150
+ setModelStatus(prev => ({
151
+ ...prev,
152
+ [mode]: { loaded: false, loading: false, error: (error as Error).message }
153
+ }));
154
+ }
155
+ };
156
+
157
+ useEffect(() => {
158
+ async function initDetector() {
159
+ const currentModel = modelStatus[mode];
160
+ if (currentModel.loaded || currentModel.loading) return;
161
+
162
+ setModelStatus(prev => ({ ...prev, [mode]: { loaded: false, loading: true } }));
163
+
164
+ try {
165
+ let detector: any;
166
+ const startTime = performance.now();
167
+
168
+ if (mode === 'object') {
169
+ detector = new ObjectDetector({
170
+ classes: selectedClasses,
171
+ mode: perfMode,
172
+ backend: backend,
173
+ confidence: 0.3,
174
+ cache: true,
175
+ });
176
+ } else if (mode === 'pose') {
177
+ detector = new PoseDetector({
178
+ detConfidence: 0.5,
179
+ poseConfidence: 0.3,
180
+ backend: backend,
181
+ cache: true,
182
+ });
183
+ } else if (mode === 'pose3d') {
184
+ detector = new Pose3DDetector({
185
+
186
+ detConfidence: 0.1,
187
+ poseConfidence: 0.3,
188
+ backend: backend,
189
+ cache: true, // Disable cache for large 3D model (352MB)
190
+ });
191
+ } else if (mode === 'animal') {
192
+ detector = new AnimalDetector({
193
+ classes: selectedAnimalClasses.length > 0 ? selectedAnimalClasses : null,
194
+ poseModelType: animalPoseModel,
195
+ detConfidence: 0.5,
196
+ poseConfidence: 0.3,
197
+ backend: backend,
198
+ cache: true, // Disable cache for large ViTPose models
199
+ });
200
+ }
201
+
202
+ await detector.init();
203
+ const loadTime = Math.round(performance.now() - startTime);
204
+
205
+ if (mode === 'object') setObjectDetector(detector);
206
+ else if (mode === 'pose') setPoseDetector(detector);
207
+ else if (mode === 'pose3d') setPose3DDetector(detector);
208
+ else if (mode === 'animal') setAnimalDetector(detector);
209
+
210
+ setModelStatus(prev => ({
211
+ ...prev,
212
+ [mode]: { loaded: true, loading: false }
213
+ }));
214
+
215
+ console.log(`${mode} detector initialized in ${loadTime}ms`);
216
+ } catch (error) {
217
+ // If WebGPU fails, try fallback to WASM
218
+ const errorMsg = (error as Error).message;
219
+ if (backend === 'webgpu' && (errorMsg.includes('WebGPU') || errorMsg.includes('not supported') || errorMsg.includes('session'))) {
220
+ console.warn('WebGPU not available, falling back to WASM...');
221
+ setBackend('wasm');
222
+
223
+ // Retry with WASM
224
+ await initDetectorWithFallback(mode, perfMode, 'wasm');
225
+ return;
226
+ }
227
+
228
+ console.error(`Failed to load ${mode} detector:`, error);
229
+ setModelStatus(prev => ({
230
+ ...prev,
231
+ [mode]: { loaded: false, loading: false, error: errorMsg }
232
+ }));
233
+ }
234
+ }
235
+
236
+ initDetector();
237
+ }, [mode, perfMode, backend, animalPoseModel]);
238
+
239
+ useEffect(() => {
240
+ if (objectDetector) {
241
+ objectDetector.setClasses(selectedClasses.length > 0 ? selectedClasses : null);
242
+ }
243
+ }, [selectedClasses, objectDetector]);
244
+
245
+ useEffect(() => {
246
+ if (animalDetector) {
247
+ animalDetector.setClasses(selectedAnimalClasses.length > 0 ? selectedAnimalClasses : null);
248
+ }
249
+ }, [selectedAnimalClasses, animalDetector]);
250
+
251
+ useEffect(() => {
252
+ async function setupCamera() {
253
+ if (useCamera && videoRef.current) {
254
+ try {
255
+ const stream = await navigator.mediaDevices.getUserMedia({
256
+ video: { width: 640, height: 480 },
257
+ });
258
+ videoRef.current.srcObject = stream;
259
+ await videoRef.current.play();
260
+ } catch (err) {
261
+ console.error('Camera error:', err);
262
+ setUseCamera(false);
263
+ }
264
+ } else if (videoRef.current?.srcObject) {
265
+ const stream = videoRef.current.srcObject as MediaStream;
266
+ stream.getTracks().forEach(track => track.stop());
267
+ videoRef.current.srcObject = null;
268
+ }
269
+ }
270
+
271
+ setupCamera();
272
+ }, [useCamera]);
273
+
274
+ const processDetection = async () => {
275
+ if (!canvasRef.current || !modelStatus[mode].loaded) return;
276
+
277
+ // Performance optimization: skip frames for video/camera
278
+ if (useCamera || videoSrc) {
279
+ frameCountRef.current++;
280
+ if (frameCountRef.current % processEveryNFrames !== 0) {
281
+ return; // Skip this frame
282
+ }
283
+ }
284
+
285
+ const canvas = canvasRef.current;
286
+ const ctx = canvas.getContext('2d', { willReadFrequently: true });
287
+ if (!ctx) return;
288
+
289
+ let results: any[] = [];
290
+ const startTime = performance.now();
291
+
292
+ if (mode === 'object' && objectDetector) {
293
+ if (useCamera && videoRef.current) {
294
+ ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
295
+ results = await objectDetector.detectFromVideo(videoRef.current, canvas);
296
+ drawDetectionsOnCanvas(ctx, results);
297
+ } else if (videoSrc && videoRef.current) {
298
+ ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
299
+ results = await objectDetector.detectFromVideo(videoRef.current, canvas);
300
+ drawDetectionsOnCanvas(ctx, results);
301
+ } else {
302
+ results = await objectDetector.detectFromCanvas(canvas);
303
+ drawDetectionsOnCanvas(ctx, results);
304
+ }
305
+ } else if (mode === 'pose' && poseDetector) {
306
+ if (useCamera && videoRef.current) {
307
+ ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
308
+ results = await poseDetector.detectFromVideo(videoRef.current, canvas);
309
+ drawPoseOnCanvas(ctx, results);
310
+ } else if (videoSrc && videoRef.current) {
311
+ ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
312
+ results = await poseDetector.detectFromVideo(videoRef.current, canvas);
313
+ drawPoseOnCanvas(ctx, results);
314
+ } else {
315
+ results = await poseDetector.detectFromCanvas(canvas);
316
+ drawPoseOnCanvas(ctx, results);
317
+ }
318
+ } else if (mode === 'pose3d' && pose3DDetector) {
319
+ if (useCamera && videoRef.current) {
320
+ ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
321
+ const result3d = await pose3DDetector.detectFromVideo(videoRef.current, canvas);
322
+ results = process3DResult(result3d);
323
+ drawPoseOnCanvas(ctx, results);
324
+ } else if (videoSrc && videoRef.current) {
325
+ ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
326
+ const result3d = await pose3DDetector.detectFromVideo(videoRef.current, canvas);
327
+ results = process3DResult(result3d);
328
+ drawPoseOnCanvas(ctx, results);
329
+ } else {
330
+ const result3d = await pose3DDetector.detectFromCanvas(canvas);
331
+ results = process3DResult(result3d);
332
+ drawPoseOnCanvas(ctx, results);
333
+ }
334
+ } else if (mode === 'animal' && animalDetector) {
335
+ if (useCamera && videoRef.current) {
336
+ ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
337
+ results = await animalDetector.detectFromVideo(videoRef.current, canvas);
338
+ drawAnimalResults(ctx, results);
339
+ } else if (videoSrc && videoRef.current) {
340
+ ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
341
+ results = await animalDetector.detectFromVideo(videoRef.current, canvas);
342
+ drawAnimalResults(ctx, results);
343
+ } else {
344
+ results = await animalDetector.detectFromCanvas(canvas);
345
+ drawAnimalResults(ctx, results);
346
+ }
347
+ }
348
+
349
+ const endTime = performance.now();
350
+ const detStats = (results as any).stats;
351
+
352
+ setDetections(results);
353
+ setStats({
354
+ time: Math.round(endTime - startTime),
355
+ count: results.length,
356
+ detTime: detStats?.detTime,
357
+ poseTime: detStats?.poseTime,
358
+ });
359
+ };
360
+
361
+ const process3DResult = (result3d: any): Detection[] => {
362
+ const keypoints = result3d.keypoints || [];
363
+ const keypoints2d = result3d.keypoints2d || [];
364
+ const scores = result3d.scores || [];
365
+ const stats = result3d.stats;
366
+
367
+ const detections: Detection[] = [];
368
+
369
+ for (let i = 0; i < keypoints.length; i++) {
370
+ const personKeypoints = keypoints[i];
371
+ const personKeypoints2d = keypoints2d[i];
372
+ const personScores = scores[i];
373
+
374
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
375
+ for (const kpt of personKeypoints2d) {
376
+ minX = Math.min(minX, kpt[0]);
377
+ minY = Math.min(minY, kpt[1]);
378
+ maxX = Math.max(maxX, kpt[0]);
379
+ maxY = Math.max(maxY, kpt[1]);
380
+ }
381
+
382
+ detections.push({
383
+ bbox: {
384
+ x1: Math.max(0, minX - 20),
385
+ y1: Math.max(0, minY - 20),
386
+ x2: Math.min(canvasRef.current?.width || 640, maxX + 20),
387
+ y2: Math.min(canvasRef.current?.height || 480, maxY + 20),
388
+ confidence: personScores.reduce((a: number, b: number) => a + b, 0) / personScores.length,
389
+ },
390
+ keypoints: personKeypoints2d.map((kpt: number[], idx: number) => ({
391
+ x: kpt[0],
392
+ y: kpt[1],
393
+ score: personScores[idx],
394
+ visible: personScores[idx] > 0.3,
395
+ })),
396
+ keypoints3d: personKeypoints,
397
+ });
398
+ }
399
+
400
+ (detections as any).stats = stats;
401
+ return detections;
402
+ };
403
+
404
+ const drawAnimalResults = (ctx: CanvasRenderingContext2D, animals: any[]) => {
405
+ const colors = ['#ff6b6b', '#51cf66', '#339af0', '#ffd43b', '#da77f2', '#ff922b'];
406
+
407
+ animals.forEach((animal, idx) => {
408
+ const color = colors[idx % colors.length];
409
+
410
+ ctx.strokeStyle = color;
411
+ ctx.lineWidth = 3;
412
+ ctx.strokeRect(
413
+ animal.bbox.x1,
414
+ animal.bbox.y1,
415
+ animal.bbox.x2 - animal.bbox.x1,
416
+ animal.bbox.y2 - animal.bbox.y1
417
+ );
418
+
419
+ ctx.fillStyle = color;
420
+ ctx.font = 'bold 14px Inter, sans-serif';
421
+ ctx.fillText(
422
+ `${animal.className} ${(animal.bbox.confidence * 100).toFixed(0)}%`,
423
+ animal.bbox.x1,
424
+ animal.bbox.y1 - 8
425
+ );
426
+
427
+ animal.keypoints?.forEach((kp: any) => {
428
+ if (kp.visible) {
429
+ ctx.fillStyle = '#51cf66';
430
+ ctx.beginPath();
431
+ ctx.arc(kp.x, kp.y, 5, 0, Math.PI * 2);
432
+ ctx.fill();
433
+ }
434
+ });
435
+ });
436
+ };
437
+
438
+ const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
439
+ const file = e.target.files?.[0];
440
+ if (!file || !canvasRef.current) return;
441
+
442
+ if (file.type.startsWith('video/')) {
443
+ const url = URL.createObjectURL(file);
444
+ setVideoSrc(url);
445
+ setHasImage(false);
446
+ setUseCamera(false);
447
+ setIsPlaying(false);
448
+
449
+ const canvas = canvasRef.current;
450
+ const ctx = canvas.getContext('2d', { willReadFrequently: true });
451
+ if (ctx) {
452
+ canvas.width = 640;
453
+ canvas.height = 480;
454
+ ctx.fillStyle = '#1e293b';
455
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
456
+ ctx.fillStyle = '#94a3b8';
457
+ ctx.font = '16px Inter, sans-serif';
458
+ ctx.textAlign = 'center';
459
+ ctx.fillText('Video loaded. Click Play to start detection', canvas.width / 2, canvas.height / 2);
460
+ }
461
+ } else {
462
+ const img = new Image();
463
+ img.onload = () => {
464
+ const canvas = canvasRef.current!;
465
+ const ctx = canvas.getContext('2d', { willReadFrequently: true })!;
466
+ canvas.width = img.naturalWidth;
467
+ canvas.height = img.naturalHeight;
468
+ ctx.drawImage(img, 0, 0);
469
+ setVideoSrc(null);
470
+ setUseCamera(false);
471
+ setHasImage(true);
472
+ setIsPlaying(false);
473
+ stopDetectionLoop();
474
+ };
475
+ img.src = URL.createObjectURL(file);
476
+ }
477
+ };
478
+
479
+ const startVideoDetection = async () => {
480
+ if (!videoRef.current || !videoSrc) return;
481
+
482
+ try {
483
+ videoRef.current.src = videoSrc;
484
+ videoRef.current.load();
485
+
486
+ await new Promise((resolve) => {
487
+ const timeout = setTimeout(resolve, 5000);
488
+ videoRef.current!.addEventListener('loadeddata', () => {
489
+ clearTimeout(timeout);
490
+ resolve(true);
491
+ }, { once: true });
492
+ });
493
+
494
+ if (canvasRef.current && videoRef.current) {
495
+ const videoWidth = videoRef.current.videoWidth || 640;
496
+ const videoHeight = videoRef.current.videoHeight || 480;
497
+ canvasRef.current.width = videoWidth;
498
+ canvasRef.current.height = videoHeight;
499
+ }
500
+
501
+ await videoRef.current.play();
502
+ setIsPlaying(true);
503
+ setHasImage(false);
504
+
505
+ const interval = setInterval(() => {
506
+ if (!videoRef.current || videoRef.current.paused || videoRef.current.ended) return;
507
+ processDetection();
508
+ }, 100);
509
+
510
+ setDetectionInterval(interval);
511
+ } catch (error) {
512
+ console.error('Error starting video:', error);
513
+ setIsPlaying(false);
514
+ }
515
+ };
516
+
517
+ const stopDetectionLoop = () => {
518
+ if (detectionInterval) {
519
+ clearInterval(detectionInterval);
520
+ setDetectionInterval(null);
521
+ }
522
+ if (videoRef.current) {
523
+ videoRef.current.pause();
524
+ }
525
+ setIsPlaying(false);
526
+ };
527
+
528
+ useEffect(() => {
529
+ return () => {
530
+ stopDetectionLoop();
531
+ if (videoSrc) URL.revokeObjectURL(videoSrc);
532
+ };
533
+ }, []);
534
+
535
+ useEffect(() => {
536
+ if (!videoRef.current) return;
537
+ const handleVideoEnd = () => {
538
+ setIsPlaying(false);
539
+ stopDetectionLoop();
540
+ };
541
+ videoRef.current.addEventListener('ended', handleVideoEnd);
542
+ return () => videoRef.current?.removeEventListener('ended', handleVideoEnd);
543
+ }, [videoSrc]);
544
+
545
+ const toggleClass = (className: string) => {
546
+ setSelectedClasses(prev =>
547
+ prev.includes(className)
548
+ ? prev.filter(c => c !== className)
549
+ : [...prev, className]
550
+ );
551
+ };
552
+
553
+ const toggleAnimalClass = (className: string) => {
554
+ setSelectedAnimalClasses(prev =>
555
+ prev.includes(className)
556
+ ? prev.filter(c => c !== className)
557
+ : [...prev, className]
558
+ );
559
+ };
560
+
561
+ useEffect(() => {
562
+ if (canvasRef.current) {
563
+ const ctx = canvasRef.current.getContext('2d');
564
+ if (ctx) {
565
+ ctx.fillStyle = '#1e293b';
566
+ ctx.fillRect(0, 0, canvasRef.current.width, canvasRef.current.height);
567
+ }
568
+ setDetections([]);
569
+ setStats(null);
570
+ }
571
+ }, [mode]);
572
+
573
+ if (modelStatus[mode].loading) {
574
+ return (
575
+ <div style={styles.container}>
576
+ <div style={styles.loadingScreen}>
577
+ <div style={styles.spinner}></div>
578
+ <h2 style={styles.loadingTitle}>Loading {mode === 'pose3d' ? '3D Pose' : mode.charAt(0).toUpperCase() + mode.slice(1)} Detector...</h2>
579
+ <p style={styles.loadingText}>This may take a moment on first load.</p>
580
+ </div>
581
+ </div>
582
+ );
583
+ }
584
+
585
+ if (modelStatus[mode].error) {
586
+ return (
587
+ <div style={styles.container}>
588
+ <div style={styles.errorScreen}>
589
+ <div style={styles.errorIcon}>❌</div>
590
+ <h2 style={styles.errorTitle}>Failed to Load Model</h2>
591
+ <p style={styles.errorText}>{modelStatus[mode].error}</p>
592
+ <button
593
+ style={styles.retryButton}
594
+ onClick={() => setModelStatus(prev => ({ ...prev, [mode]: { loaded: false, loading: true } }))}
595
+ >
596
+ Try Again
597
+ </button>
598
+ </div>
599
+ </div>
600
+ );
601
+ }
602
+
603
+ return (
604
+ <div style={styles.container}>
605
+ {/* Header */}
606
+ <header style={styles.header}>
607
+ <div style={styles.headerContent}>
608
+ <div>
609
+ <h1 style={styles.title}>🎯 rtmlib-ts Playground</h1>
610
+ <p style={styles.subtitle}>Real-time AI Vision: Object Detection, 2D/3D Pose & Animal Detection</p>
611
+ </div>
612
+ <button
613
+ style={styles.docsButton}
614
+ onClick={() => setShowDocs(!showDocs)}
615
+ >
616
+ {showDocs ? '📖 Hide Docs' : '📖 Quick Docs'}
617
+ </button>
618
+ </div>
619
+ </header>
620
+
621
+ {/* Documentation */}
622
+ {showDocs && (
623
+ <div style={styles.docsPanel}>
624
+ <div style={styles.docsGrid}>
625
+ <div style={styles.docCard}>
626
+ <h3 style={styles.docCardTitle}>🔍 Object Detection</h3>
627
+ <p style={styles.docCardText}>Detect 80 COCO classes (person, car, dog, etc.) using YOLOv12.</p>
628
+ <code style={styles.code}>new ObjectDetector({'{'} classes: ['person'] {'}'})</code>
629
+ </div>
630
+ <div style={styles.docCard}>
631
+ <h3 style={styles.docCardTitle}>🧍 Pose Estimation (2D)</h3>
632
+ <p style={styles.docCardText}>Detect 17 body keypoints using RTMW model.</p>
633
+ <code style={styles.code}>new PoseDetector()</code>
634
+ </div>
635
+ <div style={styles.docCard}>
636
+ <h3 style={styles.docCardTitle}>🎭 Pose Estimation (3D)</h3>
637
+ <p style={styles.docCardText}>3D pose estimation with Z-coordinates in meters using RTMW3D-X.</p>
638
+ <code style={styles.code}>new Pose3DDetector()</code>
639
+ </div>
640
+ <div style={styles.docCard}>
641
+ <h3 style={styles.docCardTitle}>🦁 Animal Detection</h3>
642
+ <p style={styles.docCardText}>Detect 30 animal species with ViTPose++ pose estimation.</p>
643
+ <code style={styles.code}>new AnimalDetector({'{'} poseModelType: 'vitpose-b' {'}'})</code>
644
+ </div>
645
+ </div>
646
+ </div>
647
+ )}
648
+
649
+ {/* Controls */}
650
+ <div style={styles.controls}>
651
+ <div style={styles.controlGroup}>
652
+ <label style={styles.label}>Mode</label>
653
+ <select
654
+ value={mode}
655
+ onChange={(e) => setMode(e.target.value as 'object' | 'pose' | 'pose3d' | 'animal')}
656
+ style={styles.select}
657
+ >
658
+ <option value="object">🔍 Object Detection</option>
659
+ <option value="pose">🧍 Pose Estimation (2D)</option>
660
+ <option value="pose3d">🎭 Pose Estimation (3D)</option>
661
+ <option value="animal">🦁 Animal Detection</option>
662
+ </select>
663
+ </div>
664
+
665
+ <div style={styles.controlGroup}>
666
+ <label style={styles.label}>Performance</label>
667
+ <select
668
+ value={perfMode}
669
+ onChange={(e) => setPerfMode(e.target.value as any)}
670
+ style={styles.select}
671
+ >
672
+ <option value="performance">⚡ Performance (640×640)</option>
673
+ <option value="balanced">⚖️ Balanced (416×416)</option>
674
+ <option value="lightweight">🚀 Lightweight (320×320)</option>
675
+ </select>
676
+ </div>
677
+
678
+ <div style={styles.controlGroup}>
679
+ <label style={styles.label}>Backend</label>
680
+ <select
681
+ value={backend}
682
+ onChange={(e) => setBackend(e.target.value as any)}
683
+ style={styles.select}
684
+ >
685
+ <option value="wasm">💻 WASM (CPU)</option>
686
+ <option value="webgpu">🎮 WebGPU (GPU)</option>
687
+ </select>
688
+ </div>
689
+
690
+ {/* Performance optimization: Frame skipper */}
691
+ {(useCamera || videoSrc) && (
692
+ <div style={styles.controlGroup}>
693
+ <label style={styles.label}>⚡ Process Every Nth Frame</label>
694
+ <select
695
+ value={processEveryNFrames}
696
+ onChange={(e) => setProcessEveryNFrames(Number(e.target.value))}
697
+ style={styles.select}
698
+ >
699
+ <option value={1}>Every frame (slow)</option>
700
+ <option value={2}>Every 2nd frame</option>
701
+ <option value={3}>Every 3rd frame (recommended)</option>
702
+ <option value={4}>Every 4th frame</option>
703
+ <option value={5}>Every 5th frame (fast)</option>
704
+ </select>
705
+ <small style={styles.hint}>
706
+ Higher = faster but less smooth
707
+ </small>
708
+ </div>
709
+ )}
710
+
711
+ {mode === 'animal' && (
712
+ <div style={styles.controlGroup}>
713
+ <label style={styles.label}>Animal Pose Model</label>
714
+ <select
715
+ value={animalPoseModel}
716
+ onChange={(e) => setAnimalPoseModel(e.target.value as any)}
717
+ style={styles.select}
718
+ >
719
+ {(Object.keys(VITPOSE_MODELS) as Array<keyof typeof VITPOSE_MODELS>).map((key) => (
720
+ <option key={key} value={key}>
721
+ {VITPOSE_MODELS[key].name} - {VITPOSE_MODELS[key].ap} AP
722
+ </option>
723
+ ))}
724
+ </select>
725
+ </div>
726
+ )}
727
+
728
+ <div style={styles.controlGroup}>
729
+ <label style={styles.label}>Input Source</label>
730
+ <div style={styles.buttonGroup}>
731
+ <button
732
+ onClick={() => {
733
+ setUseCamera(!useCamera);
734
+ setVideoSrc(null);
735
+ setHasImage(false);
736
+ stopDetectionLoop();
737
+ }}
738
+ style={{
739
+ ...styles.button,
740
+ background: useCamera ? 'linear-gradient(135deg, #00d9ff, #00ff88)' : undefined,
741
+ color: useCamera ? '#000' : '#fff',
742
+ }}
743
+ >
744
+ {useCamera ? '📹 Camera On' : '📷 Camera'}
745
+ </button>
746
+ {videoSrc && (
747
+ <button
748
+ onClick={isPlaying ? stopDetectionLoop : startVideoDetection}
749
+ style={{
750
+ ...styles.button,
751
+ background: isPlaying ? 'linear-gradient(135deg, #00ff88, #00d9ff)' : 'linear-gradient(135deg, #00d9ff, #00ff88)',
752
+ color: '#000',
753
+ }}
754
+ >
755
+ {isPlaying ? '⏹ Stop' : '▶ Play'}
756
+ </button>
757
+ )}
758
+ </div>
759
+ <label style={styles.fileLabel}>
760
+ 📁 Upload Image/Video
761
+ <input
762
+ type="file"
763
+ accept="image/*,video/*"
764
+ onChange={handleFileUpload}
765
+ style={{ display: 'none' }}
766
+ />
767
+ </label>
768
+ </div>
769
+
770
+ {mode === 'object' && (
771
+ <div style={styles.controlGroup}>
772
+ <label style={styles.label}>Classes</label>
773
+ <div style={styles.classGrid}>
774
+ {['person', 'car', 'dog', 'cat', 'bicycle', 'bus', 'truck'].map(cls => (
775
+ <label key={cls} style={styles.checkbox}>
776
+ <input
777
+ type="checkbox"
778
+ checked={selectedClasses.includes(cls)}
779
+ onChange={() => toggleClass(cls)}
780
+ />
781
+ {cls}
782
+ </label>
783
+ ))}
784
+ </div>
785
+ </div>
786
+ )}
787
+
788
+ {mode === 'animal' && (
789
+ <div style={styles.controlGroup}>
790
+ <label style={styles.label}>Animal Classes</label>
791
+ <div style={styles.classGrid}>
792
+ {['dog', 'cat', 'horse', 'zebra', 'elephant', 'tiger', 'lion', 'panda'].map(cls => (
793
+ <label key={cls} style={styles.checkbox}>
794
+ <input
795
+ type="checkbox"
796
+ checked={selectedAnimalClasses.includes(cls)}
797
+ onChange={() => toggleAnimalClass(cls)}
798
+ />
799
+ {cls}
800
+ </label>
801
+ ))}
802
+ </div>
803
+ <small style={styles.hint}>
804
+ {selectedAnimalClasses.length === 0 ? 'All 30 animals' : `${selectedAnimalClasses.length} selected`}
805
+ </small>
806
+ </div>
807
+ )}
808
+
809
+ <button
810
+ onClick={processDetection}
811
+ style={styles.detectButton}
812
+ >
813
+ 🚀 Run Detection
814
+ </button>
815
+ </div>
816
+
817
+ {/* Canvas */}
818
+ <div style={styles.canvasWrapper}>
819
+ <canvas ref={canvasRef} style={styles.canvas} width={640} height={480} />
820
+ <video ref={videoRef} muted style={styles.hiddenVideo} />
821
+ </div>
822
+
823
+ {/* Status badges */}
824
+ {videoSrc && (
825
+ <div style={{
826
+ ...styles.badge,
827
+ background: isPlaying ? 'linear-gradient(135deg, #00ff88, #00d9ff)' : 'linear-gradient(135deg, #ff4444, #ff6b6b)',
828
+ }}>
829
+ {isPlaying ? '🎬 Playing' : '⏸ Paused'}
830
+ </div>
831
+ )}
832
+
833
+ {mode === 'pose3d' && (
834
+ <div style={styles.infoBadge}>
835
+ 💡 3D Mode: Z-coordinates in meters
836
+ </div>
837
+ )}
838
+
839
+ {mode === 'animal' && (
840
+ <div style={styles.infoBadge}>
841
+ 🦁 Animal Mode: {VITPOSE_MODELS[animalPoseModel].name} ({VITPOSE_MODELS[animalPoseModel].ap} AP)
842
+ </div>
843
+ )}
844
+
845
+ {/* Stats */}
846
+ {stats && (
847
+ <div style={styles.statsGrid}>
848
+ <div style={styles.statCard}>
849
+ <div style={styles.statValue}>{stats.count}</div>
850
+ <div style={styles.statLabel}>Detections</div>
851
+ </div>
852
+ <div style={styles.statCard}>
853
+ <div style={styles.statValue}>{stats.time}ms</div>
854
+ <div style={styles.statLabel}>Total Time</div>
855
+ </div>
856
+ {stats.detTime && (
857
+ <div style={styles.statCard}>
858
+ <div style={styles.statValue}>{stats.detTime}ms</div>
859
+ <div style={styles.statLabel}>Detection</div>
860
+ </div>
861
+ )}
862
+ {stats.poseTime && (
863
+ <div style={styles.statCard}>
864
+ <div style={styles.statValue}>{stats.poseTime}ms</div>
865
+ <div style={styles.statLabel}>Pose</div>
866
+ </div>
867
+ )}
868
+ </div>
869
+ )}
870
+
871
+ {/* Cache */}
872
+ {cacheInfo && (
873
+ <div style={styles.cacheBar}>
874
+ <div style={styles.cacheInfo}>
875
+ <span>💾 Model Cache:</span>
876
+ <span style={styles.cacheSize}>{cacheInfo.size}</span>
877
+ <span style={styles.cacheCount}>({cacheInfo.cached} models)</span>
878
+ </div>
879
+ <button
880
+ onClick={async () => {
881
+ await clearModelCache();
882
+ setCacheInfo({ size: '0 B', cached: 0 });
883
+ setModelStatus({
884
+ object: { loaded: false, loading: false },
885
+ pose: { loaded: false, loading: false },
886
+ pose3d: { loaded: false, loading: false },
887
+ animal: { loaded: false, loading: false },
888
+ });
889
+ }}
890
+ style={styles.clearButton}
891
+ >
892
+ 🗑️ Clear Cache
893
+ </button>
894
+ </div>
895
+ )}
896
+
897
+ {/* Results */}
898
+ {detections.length > 0 && (
899
+ <div style={styles.resultsPanel}>
900
+ <h3 style={styles.resultsTitle}>📊 Detection Results</h3>
901
+ <div style={styles.resultsList}>
902
+ {detections.map((det, idx) => (
903
+ <div key={idx} style={styles.resultCard}>
904
+ <div style={styles.resultHeader}>
905
+ <span style={styles.resultName}>
906
+ {det.className || 'Person'} #{idx + 1}
907
+ </span>
908
+ <span style={styles.resultConfidence}>
909
+ {(det.bbox.confidence * 100).toFixed(1)}%
910
+ </span>
911
+ </div>
912
+ <div style={styles.resultCoords}>
913
+ [{det.bbox.x1.toFixed(0)}, {det.bbox.y1.toFixed(0)}, {det.bbox.x2.toFixed(0)}, {det.bbox.y2.toFixed(0)}]
914
+ </div>
915
+ {mode === 'pose3d' && det.keypoints3d && (
916
+ <div style={styles.result3d}>
917
+ <small>🎯 3D: [{det.keypoints3d[0][0].toFixed(3)}, {det.keypoints3d[0][1].toFixed(3)}, {det.keypoints3d[0][2].toFixed(3)}]m</small>
918
+ </div>
919
+ )}
920
+ </div>
921
+ ))}
922
+ </div>
923
+ </div>
924
+ )}
925
+ </div>
926
+ );
927
+ }
928
+
929
+ const styles: Record<string, React.CSSProperties> = {
930
+ container: {
931
+ maxWidth: 1400,
932
+ margin: '0 auto',
933
+ padding: '24px',
934
+ fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
935
+ background: 'linear-gradient(180deg, #0f172a 0%, #1e293b 100%)',
936
+ minHeight: '100vh',
937
+ },
938
+ header: {
939
+ marginBottom: '32px',
940
+ padding: '24px 32px',
941
+ background: 'linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(147, 51, 234, 0.15))',
942
+ borderRadius: '20px',
943
+ border: '1px solid rgba(255, 255, 255, 0.1)',
944
+ backdropFilter: 'blur(10px)',
945
+ },
946
+ headerContent: {
947
+ display: 'flex',
948
+ justifyContent: 'space-between',
949
+ alignItems: 'center',
950
+ flexWrap: 'wrap',
951
+ gap: '16px',
952
+ },
953
+ title: {
954
+ fontSize: '32px',
955
+ fontWeight: '800',
956
+ background: 'linear-gradient(135deg, #60a5fa, #c084fc)',
957
+ WebkitBackgroundClip: 'text',
958
+ WebkitTextFillColor: 'transparent',
959
+ margin: '0 0 8px 0',
960
+ },
961
+ subtitle: {
962
+ fontSize: '15px',
963
+ color: '#94a3b8',
964
+ margin: 0,
965
+ },
966
+ docsButton: {
967
+ padding: '12px 24px',
968
+ borderRadius: '12px',
969
+ border: '1px solid rgba(147, 51, 234, 0.3)',
970
+ background: 'linear-gradient(135deg, rgba(147, 51, 234, 0.2), rgba(59, 130, 246, 0.2))',
971
+ color: '#e2e8f0',
972
+ cursor: 'pointer',
973
+ fontSize: '14px',
974
+ fontWeight: '600',
975
+ transition: 'all 0.2s',
976
+ },
977
+ docsPanel: {
978
+ marginBottom: '32px',
979
+ padding: '24px',
980
+ background: 'rgba(30, 41, 59, 0.5)',
981
+ borderRadius: '16px',
982
+ border: '1px solid rgba(147, 51, 234, 0.2)',
983
+ },
984
+ docsGrid: {
985
+ display: 'grid',
986
+ gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
987
+ gap: '16px',
988
+ },
989
+ docCard: {
990
+ padding: '20px',
991
+ background: 'rgba(59, 130, 246, 0.1)',
992
+ borderRadius: '12px',
993
+ border: '1px solid rgba(59, 130, 246, 0.2)',
994
+ },
995
+ docCardTitle: {
996
+ fontSize: '16px',
997
+ fontWeight: '700',
998
+ color: '#e2e8f0',
999
+ margin: '0 0 8px 0',
1000
+ },
1001
+ docCardText: {
1002
+ fontSize: '14px',
1003
+ color: '#94a3b8',
1004
+ margin: '0 0 12px 0',
1005
+ },
1006
+ code: {
1007
+ display: 'block',
1008
+ padding: '10px 14px',
1009
+ background: 'rgba(0, 0, 0, 0.3)',
1010
+ borderRadius: '8px',
1011
+ fontSize: '12px',
1012
+ color: '#51cf66',
1013
+ fontFamily: '"Fira Code", monospace',
1014
+ },
1015
+ controls: {
1016
+ display: 'grid',
1017
+ gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
1018
+ gap: '20px',
1019
+ marginBottom: '32px',
1020
+ padding: '28px',
1021
+ background: 'rgba(30, 41, 59, 0.6)',
1022
+ borderRadius: '20px',
1023
+ border: '1px solid rgba(255, 255, 255, 0.1)',
1024
+ },
1025
+ controlGroup: {
1026
+ display: 'flex',
1027
+ flexDirection: 'column',
1028
+ gap: '10px',
1029
+ },
1030
+ label: {
1031
+ fontSize: '13px',
1032
+ fontWeight: '600',
1033
+ color: '#94a3b8',
1034
+ textTransform: 'uppercase',
1035
+ letterSpacing: '0.5px',
1036
+ },
1037
+ select: {
1038
+ padding: '14px 18px',
1039
+ borderRadius: '12px',
1040
+ border: '1px solid rgba(255, 255, 255, 0.15)',
1041
+ background: 'rgba(15, 23, 42, 0.8)',
1042
+ color: '#e2e8f0',
1043
+ fontSize: '14px',
1044
+ fontWeight: '500',
1045
+ cursor: 'pointer',
1046
+ transition: 'all 0.2s',
1047
+ },
1048
+ buttonGroup: {
1049
+ display: 'flex',
1050
+ gap: '10px',
1051
+ flexWrap: 'wrap',
1052
+ },
1053
+ button: {
1054
+ padding: '12px 20px',
1055
+ borderRadius: '10px',
1056
+ border: '1px solid rgba(255, 255, 255, 0.15)',
1057
+ background: 'rgba(59, 130, 246, 0.2)',
1058
+ color: '#e2e8f0',
1059
+ cursor: 'pointer',
1060
+ fontSize: '14px',
1061
+ fontWeight: '600',
1062
+ transition: 'all 0.2s',
1063
+ },
1064
+ fileLabel: {
1065
+ display: 'inline-block',
1066
+ padding: '14px 24px',
1067
+ borderRadius: '12px',
1068
+ background: 'linear-gradient(135deg, #3b82f6, #8b5cf6)',
1069
+ color: '#fff',
1070
+ cursor: 'pointer',
1071
+ textAlign: 'center',
1072
+ fontWeight: '600',
1073
+ fontSize: '14px',
1074
+ transition: 'transform 0.2s, box-shadow 0.2s',
1075
+ boxShadow: '0 4px 14px rgba(59, 130, 246, 0.4)',
1076
+ },
1077
+ classGrid: {
1078
+ display: 'grid',
1079
+ gridTemplateColumns: 'repeat(auto-fit, minmax(90px, 1fr))',
1080
+ gap: '8px',
1081
+ },
1082
+ checkbox: {
1083
+ display: 'flex',
1084
+ alignItems: 'center',
1085
+ gap: '8px',
1086
+ fontSize: '13px',
1087
+ padding: '8px 12px',
1088
+ background: 'rgba(59, 130, 246, 0.1)',
1089
+ borderRadius: '8px',
1090
+ cursor: 'pointer',
1091
+ color: '#e2e8f0',
1092
+ transition: 'background 0.2s',
1093
+ },
1094
+ hint: {
1095
+ color: '#64748b',
1096
+ fontSize: '12px',
1097
+ marginTop: '4px',
1098
+ },
1099
+ detectButton: {
1100
+ gridColumn: '1 / -1',
1101
+ padding: '18px 32px',
1102
+ borderRadius: '14px',
1103
+ border: 'none',
1104
+ background: 'linear-gradient(135deg, #3b82f6, #8b5cf6, #ec4899)',
1105
+ color: '#fff',
1106
+ cursor: 'pointer',
1107
+ fontSize: '16px',
1108
+ fontWeight: '700',
1109
+ transition: 'transform 0.2s, box-shadow 0.2s',
1110
+ boxShadow: '0 8px 24px rgba(59, 130, 246, 0.4)',
1111
+ },
1112
+ canvasWrapper: {
1113
+ position: 'relative',
1114
+ borderRadius: '20px',
1115
+ overflow: 'hidden',
1116
+ background: '#0f172a',
1117
+ marginBottom: '24px',
1118
+ boxShadow: '0 20px 60px rgba(0, 0, 0, 0.4)',
1119
+ border: '1px solid rgba(255, 255, 255, 0.1)',
1120
+ },
1121
+ canvas: {
1122
+ width: '100%',
1123
+ height: 'auto',
1124
+ display: 'block',
1125
+ },
1126
+ hiddenVideo: {
1127
+ position: 'absolute',
1128
+ top: 0,
1129
+ left: 0,
1130
+ width: 0,
1131
+ height: 0,
1132
+ opacity: 0,
1133
+ },
1134
+ badge: {
1135
+ display: 'inline-block',
1136
+ padding: '10px 20px',
1137
+ borderRadius: '10px',
1138
+ color: '#fff',
1139
+ fontWeight: '600',
1140
+ fontSize: '14px',
1141
+ marginBottom: '16px',
1142
+ boxShadow: '0 4px 14px rgba(0, 0, 0, 0.3)',
1143
+ },
1144
+ infoBadge: {
1145
+ display: 'inline-block',
1146
+ padding: '12px 20px',
1147
+ borderRadius: '12px',
1148
+ background: 'rgba(59, 130, 246, 0.15)',
1149
+ border: '1px solid rgba(59, 130, 246, 0.3)',
1150
+ color: '#60a5fa',
1151
+ marginBottom: '16px',
1152
+ fontSize: '14px',
1153
+ fontWeight: '600',
1154
+ },
1155
+ statsGrid: {
1156
+ display: 'grid',
1157
+ gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))',
1158
+ gap: '16px',
1159
+ marginBottom: '24px',
1160
+ },
1161
+ statCard: {
1162
+ padding: '24px',
1163
+ background: 'linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(147, 51, 234, 0.15))',
1164
+ borderRadius: '16px',
1165
+ textAlign: 'center',
1166
+ border: '1px solid rgba(255, 255, 255, 0.1)',
1167
+ },
1168
+ statValue: {
1169
+ fontSize: '32px',
1170
+ fontWeight: '800',
1171
+ background: 'linear-gradient(135deg, #51cf66, #00d9ff)',
1172
+ WebkitBackgroundClip: 'text',
1173
+ WebkitTextFillColor: 'transparent',
1174
+ marginBottom: '6px',
1175
+ },
1176
+ statLabel: {
1177
+ fontSize: '13px',
1178
+ color: '#94a3b8',
1179
+ fontWeight: '500',
1180
+ textTransform: 'uppercase',
1181
+ letterSpacing: '0.5px',
1182
+ },
1183
+ cacheBar: {
1184
+ display: 'flex',
1185
+ justifyContent: 'space-between',
1186
+ alignItems: 'center',
1187
+ padding: '20px 24px',
1188
+ background: 'rgba(30, 41, 59, 0.6)',
1189
+ borderRadius: '16px',
1190
+ marginBottom: '24px',
1191
+ border: '1px solid rgba(255, 255, 255, 0.1)',
1192
+ },
1193
+ cacheInfo: {
1194
+ display: 'flex',
1195
+ alignItems: 'center',
1196
+ gap: '12px',
1197
+ fontSize: '14px',
1198
+ color: '#94a3b8',
1199
+ },
1200
+ cacheSize: {
1201
+ color: '#51cf66',
1202
+ fontWeight: '700',
1203
+ fontSize: '16px',
1204
+ },
1205
+ cacheCount: {
1206
+ color: '#64748b',
1207
+ fontSize: '13px',
1208
+ },
1209
+ clearButton: {
1210
+ padding: '10px 20px',
1211
+ borderRadius: '10px',
1212
+ border: '1px solid rgba(239, 68, 68, 0.3)',
1213
+ background: 'rgba(239, 68, 68, 0.2)',
1214
+ color: '#f87171',
1215
+ cursor: 'pointer',
1216
+ fontSize: '13px',
1217
+ fontWeight: '600',
1218
+ },
1219
+ resultsPanel: {
1220
+ padding: '28px',
1221
+ background: 'rgba(30, 41, 59, 0.6)',
1222
+ borderRadius: '20px',
1223
+ border: '1px solid rgba(255, 255, 255, 0.1)',
1224
+ },
1225
+ resultsTitle: {
1226
+ fontSize: '18px',
1227
+ fontWeight: '700',
1228
+ color: '#e2e8f0',
1229
+ margin: '0 0 20px 0',
1230
+ },
1231
+ resultsList: {
1232
+ display: 'flex',
1233
+ flexDirection: 'column',
1234
+ gap: '12px',
1235
+ },
1236
+ resultCard: {
1237
+ padding: '18px 20px',
1238
+ background: 'rgba(15, 23, 42, 0.6)',
1239
+ borderRadius: '12px',
1240
+ border: '1px solid rgba(59, 130, 246, 0.2)',
1241
+ transition: 'border-color 0.2s',
1242
+ },
1243
+ resultHeader: {
1244
+ display: 'flex',
1245
+ justifyContent: 'space-between',
1246
+ alignItems: 'center',
1247
+ marginBottom: '8px',
1248
+ },
1249
+ resultName: {
1250
+ fontSize: '15px',
1251
+ fontWeight: '700',
1252
+ color: '#e2e8f0',
1253
+ },
1254
+ resultConfidence: {
1255
+ fontSize: '14px',
1256
+ fontWeight: '700',
1257
+ color: '#51cf66',
1258
+ background: 'rgba(81, 207, 102, 0.15)',
1259
+ padding: '4px 12px',
1260
+ borderRadius: '6px',
1261
+ },
1262
+ resultCoords: {
1263
+ fontSize: '13px',
1264
+ color: '#64748b',
1265
+ fontFamily: '"Fira Code", monospace',
1266
+ },
1267
+ result3d: {
1268
+ marginTop: '10px',
1269
+ padding: '10px 14px',
1270
+ background: 'rgba(59, 130, 246, 0.15)',
1271
+ borderRadius: '8px',
1272
+ color: '#60a5fa',
1273
+ fontFamily: '"Fira Code", monospace',
1274
+ fontSize: '13px',
1275
+ },
1276
+ loadingScreen: {
1277
+ display: 'flex',
1278
+ flexDirection: 'column',
1279
+ alignItems: 'center',
1280
+ justifyContent: 'center',
1281
+ minHeight: '80vh',
1282
+ gap: '24px',
1283
+ },
1284
+ spinner: {
1285
+ width: '60px',
1286
+ height: '60px',
1287
+ border: '4px solid rgba(59, 130, 246, 0.2)',
1288
+ borderTop: '4px solid #3b82f6',
1289
+ borderRadius: '50%',
1290
+ animation: 'spin 1s linear infinite',
1291
+ },
1292
+ loadingTitle: {
1293
+ fontSize: '24px',
1294
+ fontWeight: '700',
1295
+ color: '#e2e8f0',
1296
+ margin: '0 0 8px 0',
1297
+ },
1298
+ loadingText: {
1299
+ fontSize: '15px',
1300
+ color: '#94a3b8',
1301
+ margin: 0,
1302
+ },
1303
+ errorScreen: {
1304
+ display: 'flex',
1305
+ flexDirection: 'column',
1306
+ alignItems: 'center',
1307
+ justifyContent: 'center',
1308
+ minHeight: '80vh',
1309
+ gap: '24px',
1310
+ },
1311
+ errorIcon: {
1312
+ fontSize: '48px',
1313
+ },
1314
+ errorTitle: {
1315
+ fontSize: '24px',
1316
+ fontWeight: '700',
1317
+ color: '#f87171',
1318
+ margin: '0 0 8px 0',
1319
+ },
1320
+ errorText: {
1321
+ fontSize: '15px',
1322
+ color: '#94a3b8',
1323
+ margin: '0 0 20px 0',
1324
+ textAlign: 'center',
1325
+ maxWidth: '400px',
1326
+ },
1327
+ retryButton: {
1328
+ padding: '14px 32px',
1329
+ borderRadius: '12px',
1330
+ border: 'none',
1331
+ background: 'linear-gradient(135deg, #3b82f6, #8b5cf6)',
1332
+ color: '#fff',
1333
+ cursor: 'pointer',
1334
+ fontSize: '16px',
1335
+ fontWeight: '700',
1336
+ boxShadow: '0 8px 24px rgba(59, 130, 246, 0.4)',
1337
+ },
1338
+ };