33< head >
44 < meta charset ="UTF-8 " />
55 < meta name ="viewport " content ="width=device-width, initial-scale=1.0 " />
6- < title > Random Collection Entry</ title >
7- < style >
8- body {
9- margin : 0 ;
10- min-height : 100vh ;
11- display : flex;
12- justify-content : center;
13- align-items : center;
14- font-family : Arial, Helvetica, sans-serif;
15- background-color : # f5f5f5 ;
16- }
6+ < title > ML Image Similarity Browser</ title >
177
18- . container {
19- text-align : center;
20- }
8+ <!-- TensorFlow.js + MobileNet -->
9+ < script src =" https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.16.0 " > </ script >
10+ < script src =" https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet@2.1.0 " > </ script >
2111
22- h1 {
23- margin-bottom : 20px ;
24- }
25-
26- img {
27- max-width : 90vw ;
28- max-height : 70vh ;
29- object-fit : contain;
30- border-radius : 8px ;
31- box-shadow : 0 4px 12px rgba (0 , 0 , 0 , 0.15 );
32- }
33- </ style >
34- </ head >
35- < body >
36- < div class ="container ">
37- < h1 id ="title "> Loading...</ h1 >
38- < img id ="image " alt ="Random collection item " />
39- </ div >
40-
41- < script >
42- fetch ( 'https:/<!DOCTYPE html>
43- < html lang = "en" >
44- < head >
45- < meta charset = "UTF-8" />
46- < meta name = "viewport" content = "width=device-width, initial-scale=1.0" />
47- < title > Smart Collection Browser</ title >
4812 < style >
4913 body {
5014 margin : 0 ;
@@ -72,16 +36,11 @@ <h1 id="title">Loading...</h1>
7236 transform : scale (0.95 );
7337 }
7438
75- h1 {
76- margin - bottom : 20 px ;
77- }
78-
7939 .main img {
8040 max-width : 60vw ;
8141 max-height : 70vh ;
82- object - fit : contain ;
8342 border-radius : 8px ;
84- box - shadow : 0 4 px 12 px rgba ( 0 , 0 , 0 , 0.15 ) ;
43+ box-shadow : 0 4px 12px rgba (0 , 0 , 0 , 0.15 );
8544 }
8645
8746 .similar {
@@ -108,43 +67,51 @@ <h1 id="title">Loading...</h1>
10867< body >
10968 < div class ="layout ">
11069 < div class ="main " id ="main ">
111- < h1 id = "title" > Loading... </ h1 >
112- < img id = "image" />
70+ < h1 id ="title "> Loading model… </ h1 >
71+ < img id ="image " crossorigin =" anonymous " />
11372 </ div >
11473 < div class ="similar " id ="similar "> </ div >
11574 </ div >
11675
11776 < script >
11877 let collection = [ ] ;
78+ let model ;
11979 let currentIndex = 0 ;
12080
121- // --- Simple ML: TF-IDF cosine similarity on titles ---
122- function tokenize(text) {
123- return text . toLowerCase ( ) . split ( / \W + / ) . filter ( Boolean ) ;
124- }
125-
126- function buildTf(tokens) {
127- const tf = { } ;
128- tokens . forEach ( t => tf [ t ] = ( tf [ t ] || 0 ) + 1 ) ;
129- return tf ;
81+ async function loadModel ( ) {
82+ model = await mobilenet . load ( { version : 2 , alpha : 1.0 } ) ;
83+ }
84+
85+ async function embedImage ( url ) {
86+ return new Promise ( resolve => {
87+ const img = new Image ( ) ;
88+ img . crossOrigin = 'anonymous' ;
89+ img . src = url ;
90+ img . onload = async ( ) => {
91+ const embedding = model . infer ( img , true ) ;
92+ const values = await embedding . data ( ) ;
93+ embedding . dispose ( ) ;
94+ resolve ( Array . from ( values ) ) ;
95+ } ;
96+ } ) ;
13097 }
13198
13299 function cosine ( a , b ) {
133100 let dot = 0 , magA = 0 , magB = 0 ;
134- for ( const k in a ) {
135- if ( b [ k ] ) dot += a [ k ] * b [ k ] ;
136- magA + = a [ k ] * a [ k ] ;
101+ for ( let i = 0 ; i < a . length ; i ++ ) {
102+ dot += a [ i ] * b [ i ] ;
103+ magA += a [ i ] * a [ i ] ;
104+ magB += b [ i ] * b [ i ] ;
137105 }
138- for ( const k in b ) magB += b [ k ] * b [ k ] ;
139- return dot / ( Math . sqrt ( magA ) * Math . sqrt ( magB ) || 1 ) ;
106+ return dot / ( Math . sqrt ( magA ) * Math . sqrt ( magB ) ) ;
140107 }
141108
142109 function findSimilar ( index , count = 4 ) {
143- const baseTf = collection [ index ] . tf ;
110+ const base = collection [ index ] . embedding ;
144111 return collection
145112 . map ( ( item , i ) => ( {
146113 index : i ,
147- score : i === index ? - 1 : cosine ( baseTf , item . tf )
114+ score : i === index ? - 1 : cosine ( base , item . embedding )
148115 } ) )
149116 . sort ( ( a , b ) => b . score - a . score )
150117 . slice ( 0 , count ) ;
@@ -160,36 +127,38 @@ <h1 id="title">Loading...</h1>
160127 document . getElementById ( 'title' ) . textContent = item . Title ;
161128 document . getElementById ( 'image' ) . src = item [ 'Image URL' ] ;
162129
163- const similarDiv = document . getElementById ( 'similar' ) ;
164- similarDiv . innerHTML = '' ;
130+ const similar = document . getElementById ( 'similar' ) ;
131+ similar . innerHTML = '' ;
165132
166133 findSimilar ( index ) . forEach ( sim => {
167134 const img = document . createElement ( 'img' ) ;
168135 img . src = collection [ sim . index ] [ 'Image URL' ] ;
169136 img . title = collection [ sim . index ] . Title ;
170137 img . onclick = ( ) => render ( sim . index ) ;
171- similarDiv . appendChild ( img ) ;
138+ similar . appendChild ( img ) ;
172139 } ) ;
173140
174141 main . classList . remove ( 'fade' ) ;
175142 } , 300 ) ;
176143 }
177144
178- fetch('https://johnstack.github.io/JavaScript-Sandpit/ng_sandbox/collection.json')
179- .then(r => r . json ( ) )
180- . then ( data => {
181- collection = data . map ( item => ( {
182- ...item ,
183- tf : buildTf ( tokenize ( item . Title || '' ) )
184- } ) ) ;
185-
186- const start = Math . floor ( Math . random ( ) * collection . length ) ;
187- render ( start ) ;
188- } )
189- .catch(err => {
190- document . getElementById ( 'title' ) . textContent = 'Failed to load collection' ;
191- console . error ( err ) ;
192- } );
145+ async function init ( ) {
146+ await loadModel ( ) ;
147+
148+ const data = await fetch ( 'https://johnstack.github.io/JavaScript-Sandpit/ng_sandbox/collection.json' ) . then ( r => r . json ( ) ) ;
149+ document . getElementById ( 'title' ) . textContent = 'Analyzing images…' ;
150+
151+ collection = [ ] ;
152+ for ( const item of data ) {
153+ const embedding = await embedImage ( item [ 'Image URL' ] ) ;
154+ collection . push ( { ...item , embedding } ) ;
155+ }
156+
157+ const start = Math . floor ( Math . random ( ) * collection . length ) ;
158+ render ( start ) ;
159+ }
160+
161+ init ( ) ;
193162 </ script >
194163</ body >
195164</ html >
0 commit comments