@@ -52,7 +52,7 @@ document.addEventListener('DOMContentLoaded', async function() {
5252 // ══════════════════════════════════════════════════
5353 // STATE
5454 // ══════════════════════════════════════════════════
55- let engine = 'roboflow-cloud ' ; // 'roboflow-cloud' | 'moondream'
55+ let engine = 'onnx-local ' ; // 'onnx-local' | 'roboflow-cloud' | 'moondream'
5656 let mode = 'camera' ;
5757 let moondreamClient = null ;
5858 let currentStream = null ;
@@ -65,10 +65,18 @@ document.addEventListener('DOMContentLoaded', async function() {
6565 let uploadedImages = [ ] ;
6666 let uploadIndex = 0 ;
6767
68- // Roboflow API key — used for both cloud and local inference
68+ // Roboflow API key — used for cloud inference
6969 var ROBOFLOW_DEFAULT_KEY = 'eMRExtPvBQ73dtzKu8Yu' ;
7070
71-
71+ // Local ONNX player detection model
72+ var playerOnnxModel = null ;
73+ var playerModelLoaded = false ;
74+ var PLAYER_ONNX_PATH = 'model/rfdetr-player.onnx' ;
75+ var PLAYER_CLASS_NAMES = {
76+ 0 : 'ball' , 1 : 'ball-in-basket' , 2 : 'number' , 3 : 'player' ,
77+ 4 : 'player-in-possession' , 5 : 'player-jump-shot' , 6 : 'player-layup-dunk' ,
78+ 7 : 'player-shot-block' , 8 : 'referee' , 9 : 'rim' , 10 : 'background'
79+ } ;
7280
7381 // FPS tracking
7482 let fpsHistory = [ ] ;
@@ -324,6 +332,73 @@ document.addEventListener('DOMContentLoaded', async function() {
324332 }
325333 }
326334
335+ // ══════════════════════════════════════════════════
336+ // LOCAL ONNX PLAYER DETECTION
337+ // ══════════════════════════════════════════════════
338+ async function loadPlayerModel ( ) {
339+ if ( playerModelLoaded ) return true ;
340+ updateStatus ( 'Loading local ONNX model...' ) ;
341+ window . reasoningConsole . logInfo ( 'Loading player detection ONNX model...' ) ;
342+
343+ playerOnnxModel = new OnnxModelRunner ( PLAYER_ONNX_PATH , {
344+ inputWidth : 640 ,
345+ inputHeight : 640 ,
346+ task : 'detect' ,
347+ classNames : PLAYER_CLASS_NAMES
348+ } ) ;
349+
350+ var loaded = await playerOnnxModel . load ( function ( msg ) {
351+ updateStatus ( 'ONNX: ' + msg ) ;
352+ } ) ;
353+
354+ playerModelLoaded = loaded ;
355+ if ( loaded ) {
356+ window . reasoningConsole . logInfo ( 'Player ONNX model loaded' ) ;
357+ updateStatus ( 'Local model ready' ) ;
358+ } else {
359+ window . reasoningConsole . logError ( 'Player ONNX model failed to load' ) ;
360+ updateStatus ( 'Local model failed — try Cloud engine' , true ) ;
361+ }
362+ return loaded ;
363+ }
364+
365+ async function onnxLocalDetect ( ) {
366+ if ( ! playerModelLoaded ) {
367+ var ok = await loadPlayerModel ( ) ;
368+ if ( ! ok ) throw new Error ( 'Local ONNX model not available' ) ;
369+ }
370+
371+ var source = ( mode === 'sample' ) ? sampleVideo : video ;
372+ var confidence = parseInt ( confidenceSlider . value ) / 100 ;
373+ var startTime = Date . now ( ) ;
374+
375+ var result = await playerOnnxModel . infer ( source , confidence ) ;
376+ var latency = Date . now ( ) - startTime ;
377+
378+ // Parse into standard format
379+ var players = [ ] ;
380+ var numbers = [ ] ;
381+ var others = [ ] ;
382+
383+ ( result . detections || [ ] ) . forEach ( function ( det ) {
384+ if ( ROBOFLOW_PLAYER_CLASSES . indexOf ( det . class ) !== - 1 ) {
385+ players . push ( det ) ;
386+ } else if ( det . class === ROBOFLOW_NUMBER_CLASS ) {
387+ numbers . push ( det ) ;
388+ } else {
389+ others . push ( det ) ;
390+ }
391+ } ) ;
392+
393+ var vw = source . videoWidth || 640 ;
394+ var vh = source . videoHeight || 480 ;
395+
396+ if ( framesAnalyzed % 20 === 0 || latency > 500 ) {
397+ window . reasoningConsole . logInfo ( 'ONNX Local: ' + players . length + ' players, ' + numbers . length + ' numbers (' + latency + 'ms)' ) ;
398+ }
399+ return { players : players , numbers : numbers , others : others , imageWidth : vw , imageHeight : vh } ;
400+ }
401+
327402 // ══════════════════════════════════════════════════
328403 // FPS TRACKING
329404 // ══════════════════════════════════════════════════
@@ -578,7 +653,9 @@ document.addEventListener('DOMContentLoaded', async function() {
578653 try {
579654 // Step 1: Detect — dispatch to selected engine
580655 var detResult ;
581- if ( engine === 'roboflow-cloud' ) {
656+ if ( engine === 'onnx-local' ) {
657+ detResult = await onnxLocalDetect ( ) ;
658+ } else if ( engine === 'roboflow-cloud' ) {
582659 detResult = await roboflowCloudDetect ( imageBase64 ) ;
583660 } else {
584661 detResult = await moondreamDetect ( imageBase64 ) ;
@@ -588,7 +665,9 @@ document.addEventListener('DOMContentLoaded', async function() {
588665 updateFPS ( ) ;
589666
590667 // Step 2: Team clustering via K-means on uniform colors
591-
668+ if ( ! imageBase64 || engine === 'onnx-local' ) {
669+ imageBase64 = captureFrame ( ) ;
670+ }
592671 var colors = detResult . players . map ( function ( p ) {
593672 return sampleDominantColor ( imageBase64 , p ) ;
594673 } ) ;
@@ -604,8 +683,10 @@ document.addEventListener('DOMContentLoaded', async function() {
604683 updateTrackedPlayers ( detResult . players , teamAssignments , numberPairs , imageBase64 ) ;
605684
606685 // Step 5: OCR jersey numbers
607- if ( engine === 'roboflow-cloud' && Object . keys ( numberPairs ) . length > 0 && moondreamClient ) {
608- ocrNumberCrops ( imageBase64 , detResult . players , numberPairs ) ;
686+ if ( ( engine === 'roboflow-cloud' || engine === 'onnx-local' ) && Object . keys ( numberPairs ) . length > 0 && moondreamClient ) {
687+ if ( engine === 'roboflow-cloud' || framesAnalyzed % 15 === 0 ) {
688+ ocrNumberCrops ( imageBase64 , detResult . players , numberPairs ) ;
689+ }
609690 } else if ( engine === 'moondream' ) {
610691 var unconfirmed = trackedPlayers . filter ( function ( tp ) { return ! tp . confirmedNumber ; } ) . slice ( 0 , 3 ) ;
611692 for ( var i = 0 ; i < unconfirmed . length ; i ++ ) {
@@ -635,8 +716,12 @@ document.addEventListener('DOMContentLoaded', async function() {
635716
636717 // Schedule next frame
637718 if ( running ) {
638- var interval = parseInt ( intervalSelect . value ) || 500 ;
639- analysisTimeout = setTimeout ( analyzeFrame , interval ) ;
719+ var interval = parseInt ( intervalSelect . value ) ;
720+ if ( interval === 0 && engine === 'onnx-local' ) {
721+ requestAnimationFrame ( function ( ) { analyzeFrame ( ) ; } ) ;
722+ } else {
723+ analysisTimeout = setTimeout ( analyzeFrame , interval || 200 ) ;
724+ }
640725 }
641726 }
642727
@@ -951,6 +1036,14 @@ document.addEventListener('DOMContentLoaded', async function() {
9511036 // ══════════════════════════════════════════════════
9521037 async function startAnalysis ( ) {
9531038 // Validate engine requirements
1039+ if ( engine === 'onnx-local' ) {
1040+ var ok = await loadPlayerModel ( ) ;
1041+ if ( ! ok ) {
1042+ window . reasoningConsole . logInfo ( 'Local ONNX failed, falling back to Cloud' ) ;
1043+ engine = 'roboflow-cloud' ;
1044+ switchEngine ( 'roboflow-cloud' ) ;
1045+ }
1046+ }
9541047 if ( engine === 'roboflow-cloud' ) {
9551048 if ( ! window . apiKeyManager . hasRoboflowKey ( ) && ! ROBOFLOW_DEFAULT_KEY ) {
9561049 window . apiKeyManager . showModal ( ) ;
@@ -1045,9 +1138,10 @@ document.addEventListener('DOMContentLoaded', async function() {
10451138
10461139 function switchEngine ( eng ) {
10471140 engine = eng ;
1048- document . getElementById ( 'engineRoboflowBtn' ) . classList . toggle ( 'active' , engine === 'roboflow-cloud' ) ;
1141+ document . getElementById ( 'engineLocalBtn' ) . classList . toggle ( 'active' , engine === 'onnx-local' ) ;
1142+ document . getElementById ( 'engineCloudBtn' ) . classList . toggle ( 'active' , engine === 'roboflow-cloud' ) ;
10491143 engineMoondreamBtn . classList . toggle ( 'active' , engine === 'moondream' ) ;
1050- roboflowInfo . style . display = engine === 'roboflow-cloud' ? '' : 'none' ;
1144+ roboflowInfo . style . display = ( engine === 'roboflow-cloud' || engine === 'onnx-local' ) ? '' : 'none' ;
10511145 window . reasoningConsole . logInfo ( 'Switched to ' + engine + ' engine' ) ;
10521146 }
10531147
@@ -1604,7 +1698,8 @@ document.addEventListener('DOMContentLoaded', async function() {
16041698 modeCameraBtn . addEventListener ( 'click' , function ( ) { switchMode ( 'camera' ) ; } ) ;
16051699 modeSampleBtn . addEventListener ( 'click' , function ( ) { switchMode ( 'sample' ) ; } ) ;
16061700 modeUploadBtn . addEventListener ( 'click' , function ( ) { switchMode ( 'upload' ) ; } ) ;
1607- document . getElementById ( 'engineRoboflowBtn' ) . addEventListener ( 'click' , function ( ) { switchEngine ( 'roboflow-cloud' ) ; } ) ;
1701+ document . getElementById ( 'engineLocalBtn' ) . addEventListener ( 'click' , function ( ) { switchEngine ( 'onnx-local' ) ; } ) ;
1702+ document . getElementById ( 'engineCloudBtn' ) . addEventListener ( 'click' , function ( ) { switchEngine ( 'roboflow-cloud' ) ; } ) ;
16081703 engineMoondreamBtn . addEventListener ( 'click' , function ( ) { switchEngine ( 'moondream' ) ; } ) ;
16091704 cameraSelect . addEventListener ( 'change' , function ( ) { if ( cameraSelect . value ) startCamera ( cameraSelect . value ) ; } ) ;
16101705 refreshCamerasBtn . addEventListener ( 'click' , enumerateCameras ) ;
0 commit comments