From d6a69cf90695ee0700725e98835feb7d1f9e2852 Mon Sep 17 00:00:00 2001 From: yankun Date: Sun, 26 Jul 2026 20:58:27 +0800 Subject: [PATCH] Add AI face capability demos --- .../demo-ai-face-detector/README.md | 55 +++ .../demo-ai-face-detector/index.html | 12 + .../demo-ai-face-detector/package.json | 33 ++ .../demo-ai-face-detector/postcss.config.js | 7 + .../demo-ai-face-detector/public/config.json | 9 + .../public/thumbnail.png | Bin 0 -> 24064 bytes .../demo-ai-face-detector/src/App.vue | 342 +++++++++++++++ .../src/assets/cutos.png | Bin 0 -> 7773 bytes .../src/components/CutosTopbar.vue | 35 ++ .../demo-ai-face-detector/src/main.ts | 5 + .../demo-ai-face-detector/src/style.css | 10 + .../demo-ai-face-detector/src/utils/config.ts | 38 ++ .../demo-ai-face-detector/src/vite-env.d.ts | 1 + .../demo-ai-face-detector/tailwind.config.js | 6 + .../demo-ai-face-detector/tsconfig.json | 20 + .../demo-ai-face-detector/vite.config.ts | 27 ++ .../demo-ai-face-service/README.md | 20 + .../demo-ai-face-service/index.html | 13 + .../demo-ai-face-service/package.json | 32 ++ .../demo-ai-face-service/postcss.config.js | 7 + .../demo-ai-face-service/public/config.json | 9 + .../demo-ai-face-service/public/thumbnail.png | Bin 0 -> 13163 bytes .../demo-ai-face-service/src/App.vue | 396 ++++++++++++++++++ .../src/camera-overlay.css | 113 +++++ .../demo-ai-face-service/src/gw-client.d.ts | 18 + .../demo-ai-face-service/src/main.ts | 6 + .../demo-ai-face-service/src/style.css | 1 + .../demo-ai-face-service/src/utils/config.ts | 6 + .../demo-ai-face-service/src/vite-env.d.ts | 2 + .../demo-ai-face-service/tailwind.config.js | 6 + .../demo-ai-face-service/tsconfig.json | 19 + .../demo-ai-face-service/vite.config.ts | 18 + 32 files changed, 1266 insertions(+) create mode 100644 ai-capabilities/demo-ai-face-detector/README.md create mode 100644 ai-capabilities/demo-ai-face-detector/index.html create mode 100644 ai-capabilities/demo-ai-face-detector/package.json create mode 100644 ai-capabilities/demo-ai-face-detector/postcss.config.js create mode 100644 ai-capabilities/demo-ai-face-detector/public/config.json create mode 100644 ai-capabilities/demo-ai-face-detector/public/thumbnail.png create mode 100644 ai-capabilities/demo-ai-face-detector/src/App.vue create mode 100644 ai-capabilities/demo-ai-face-detector/src/assets/cutos.png create mode 100644 ai-capabilities/demo-ai-face-detector/src/components/CutosTopbar.vue create mode 100644 ai-capabilities/demo-ai-face-detector/src/main.ts create mode 100644 ai-capabilities/demo-ai-face-detector/src/style.css create mode 100644 ai-capabilities/demo-ai-face-detector/src/utils/config.ts create mode 100644 ai-capabilities/demo-ai-face-detector/src/vite-env.d.ts create mode 100644 ai-capabilities/demo-ai-face-detector/tailwind.config.js create mode 100644 ai-capabilities/demo-ai-face-detector/tsconfig.json create mode 100644 ai-capabilities/demo-ai-face-detector/vite.config.ts create mode 100644 ai-capabilities/demo-ai-face-service/README.md create mode 100644 ai-capabilities/demo-ai-face-service/index.html create mode 100644 ai-capabilities/demo-ai-face-service/package.json create mode 100644 ai-capabilities/demo-ai-face-service/postcss.config.js create mode 100644 ai-capabilities/demo-ai-face-service/public/config.json create mode 100644 ai-capabilities/demo-ai-face-service/public/thumbnail.png create mode 100644 ai-capabilities/demo-ai-face-service/src/App.vue create mode 100644 ai-capabilities/demo-ai-face-service/src/camera-overlay.css create mode 100644 ai-capabilities/demo-ai-face-service/src/gw-client.d.ts create mode 100644 ai-capabilities/demo-ai-face-service/src/main.ts create mode 100644 ai-capabilities/demo-ai-face-service/src/style.css create mode 100644 ai-capabilities/demo-ai-face-service/src/utils/config.ts create mode 100644 ai-capabilities/demo-ai-face-service/src/vite-env.d.ts create mode 100644 ai-capabilities/demo-ai-face-service/tailwind.config.js create mode 100644 ai-capabilities/demo-ai-face-service/tsconfig.json create mode 100644 ai-capabilities/demo-ai-face-service/vite.config.ts diff --git a/ai-capabilities/demo-ai-face-detector/README.md b/ai-capabilities/demo-ai-face-detector/README.md new file mode 100644 index 0000000..18eb4df --- /dev/null +++ b/ai-capabilities/demo-ai-face-detector/README.md @@ -0,0 +1,55 @@ +# demo-ai-face-detector + +CUTOS 4.0 Vue 3/Vite LWA demonstrating local browser-side face detection with `@cutos/ai-face-detector`. + +No Device Provider or Gateway service is required. The LWA needs camera permission and packages the MediaPipe model/WASM resources for offline use. + +## Behaviour + +- Starts the user-facing camera automatically. +- Uses the source camera ratio in landscape and a centre-cropped 1:1 preview in portrait. +- Guides the user with a circular capture area and a moving confidence box. +- Requires one face, confidence ≥ 0.90, face size ≥ 12%, and horizontal centering. +- Requires the accepted face position and size to remain stable for about two seconds. +- Locks the stable face image and stops inference while keeping the camera preview visible. +- Restarts detection automatically after ten seconds, or immediately when `Detect again` is selected. +- Displays the locked face in the lower-right corner and keeps Detection Result and Activity in two equal cards. + +## Development with the sibling SDK + +```sh +cd ../sdk +npm install +npm run build + +cd ../demo-ai-face-detector +npm install +npm run dev +``` + +Both `dev` and the CUTOS build copy version-matched model assets through the SDK's asset command. + +## Build and package + +```sh +npx cutos lwa build +``` + +The generated package is: + +```text +release/demo-ai-face-detector-v.lwa +``` + +The LWA version comes from `public/config.json`; `package.json` intentionally remains `0.0.0`. + +## Upload and publish + +```sh +npx cutos login --username office +npx cutos lwa upload +npx cutos lwa list demo-ai-face-detector +npx cutos lwa publish --lwa --device +``` + +For a release build, depend on the published npm SDK rather than `file:../sdk`. diff --git a/ai-capabilities/demo-ai-face-detector/index.html b/ai-capabilities/demo-ai-face-detector/index.html new file mode 100644 index 0000000..ee1d867 --- /dev/null +++ b/ai-capabilities/demo-ai-face-detector/index.html @@ -0,0 +1,12 @@ + + + + + + CUTOS AI Face Detector + + +
+ + + diff --git a/ai-capabilities/demo-ai-face-detector/package.json b/ai-capabilities/demo-ai-face-detector/package.json new file mode 100644 index 0000000..0b8833d --- /dev/null +++ b/ai-capabilities/demo-ai-face-detector/package.json @@ -0,0 +1,33 @@ +{ + "name": "demo-ai-face-detector", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "assets": "cutos-ai-face-detector-assets public/cutos-ai-face-detector", + "dev": "npm run assets && vite", + "build": "cutos lwa build", + "build:app": "npm run assets && vue-tsc --noEmit && vite build", + "package": "cutos lwa package", + "validate": "cutos lwa validate", + "preview": "vite preview" + }, + "dependencies": { + "@cutos/ai-face-detector": "^4.0.1", + "@cutos/core": "^4.0.8", + "vue": "^3.4.31" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.5", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.40", + "postcss-import": "^16.1.0", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.5", + "tailwindcss": "^3.4.7", + "typescript": "^5.5.4", + "unplugin-auto-import": "^0.18.0", + "vite": "^5.3.4", + "vue-tsc": "^2.0.26" + } +} diff --git a/ai-capabilities/demo-ai-face-detector/postcss.config.js b/ai-capabilities/demo-ai-face-detector/postcss.config.js new file mode 100644 index 0000000..9d55d43 --- /dev/null +++ b/ai-capabilities/demo-ai-face-detector/postcss.config.js @@ -0,0 +1,7 @@ +export default { + plugins: { + 'postcss-import': {}, + tailwindcss: {}, + autoprefixer: {} + } +} diff --git a/ai-capabilities/demo-ai-face-detector/public/config.json b/ai-capabilities/demo-ai-face-detector/public/config.json new file mode 100644 index 0000000..d914f56 --- /dev/null +++ b/ai-capabilities/demo-ai-face-detector/public/config.json @@ -0,0 +1,9 @@ +{ + "name": "demo-ai-face-detector", + "version": "0.2.6", + "description": "CUTOS 4.0 browser-side AI face detector demo", + "params": { + "title": "CUTOS AI Face Detector", + "host": "localhost" + } +} diff --git a/ai-capabilities/demo-ai-face-detector/public/thumbnail.png b/ai-capabilities/demo-ai-face-detector/public/thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..83da7faf15c31176125f9a56c4dba8e8c21e5a2b GIT binary patch literal 24064 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 literal 0 HcmV?d00001 diff --git a/ai-capabilities/demo-ai-face-detector/src/App.vue b/ai-capabilities/demo-ai-face-detector/src/App.vue new file mode 100644 index 0000000..1f27c6a --- /dev/null +++ b/ai-capabilities/demo-ai-face-detector/src/App.vue @@ -0,0 +1,342 @@ + + + diff --git a/ai-capabilities/demo-ai-face-detector/src/assets/cutos.png b/ai-capabilities/demo-ai-face-detector/src/assets/cutos.png new file mode 100644 index 0000000000000000000000000000000000000000..9f73429832a319c18f3557b88adc7567d2114347 GIT binary patch literal 7773 zcmd^Ei$Bx*|KCMcB+0FqOL8ZY!p!9eg(CO+eIdufEX;kYZ=o{B{Z54t=9ar=v@FAP zB$q7aGE45aax1@`I*;%7kN7f1cz6+-F~aXzOcN00bIzfqlc_XJBps0MsROA9x*M-yaJ$aY6$CC*J(?22z5` zZvg;e8>U7Ewix&2&yZma<4_Xn+*rH3cd;YFn0!UxvZDc?m8F?L9X}9$)JyI_l~J`4VF87 zZo`xmPX+&ZbczGd`3*?O-dFQw8fu7dxWxwkHEV7V4XE~0W^J=R!Q6Qm_ZAZu2KVA* zQ>rqI7Me&l*kn*Al{cY~EWBufl>j3}J;-Ped%j>VPd|neNr5D>@nEr60(TRn%JB`L zQm`*&`VpJdXSlRY%GvF0kW_0pr7q*}?uTd|W@nz}uw#tZJR+m;^(&e!%d-z^$@=Mj80fZ2=>-nUNg=E?QGT9C55~ zm{}DZll|_L`qQk#g2P>dvG|Ng-W!86f2!ss23+PJeS5}yM&!B?b?Ix{_9{ZKAvwj- zLnYaZ>a9ak$4$)xlP#-m5(CBWmk2^!p_m>c@mm3}^)&+L)|DUsrgw^*;X-nCM1zh6 z2pwDEe?fd!jx7Him)NJ)_9)3?{m9Joh#RT)Cxaar@Lf~2vz@9EilVH#^SS?0JI{y& zT*X0v`hx|z*<)If6dj@h;3siut*E~IYJ;12UWuSp*!^q%IjuV!wZ`Hsshk#jb(C9v zsWgG)OWF_dlU4CeQq81j+mj4scQlss$rqdA=hVz{TQ_ir4-Sm7apen`cAZIMHB`0l zOq&+>P1n8?KkDurEc9hz7ny3wrWGS=FX`(!m=N)rk{r;_XTulrE9EOg4s}+HFZ@em z?m8=;Pm&$9ue0BxMJypsE*jwY(eZxs6%q7QU)d4A4C`~146M%2Xa8#VK!!l#Yh6F{ zILVNgHtLyU<~GtQSG~AYL{asZ#lovd$4#0%h$jp${wu)L6<5Z3=s+&sXmk`}WVE8y zg@VQc3u{!@`h_=PKyeSk*Ksrrhj?lwBu5Eyv1W>_#U|- z%;~uz{#uV28i;Isn&wYD(IJX)uMj-Vy15EEn9VJM1Eavn9PXAhE{VOngz_%Tk7&1t zp1*^LlWk&XL}0Mw6!<3(1{HxPZ{Bk7jNf?DdT00hYi}U|4BwhN!v*&Kk8jjbUn^|o z3ESfBKBjb~Ay&6o+Rk68Fg(s~qG|Y168HqcE>0}wir2Q4EQFw__Bs+x)H|a90tdZv>6OIG7f5A_GqrA*q}Hm=qsOBo&=L|@R%y+Y zBsvWKVH`e4o8J7&438A-bQ;z+B1)yh}Q$t^(d<@ zIkvD47RR?AtzxG9*1qz4Tdg}y`4eZ%cEe&llO-P2aR%hkEsB0Q&Ej zzUd(cy;fQ7%LfI+m{t!7#&#<>V`dj~yW{I%UA#bi4?vDdyL5W%jHFdypESvLSHplHqTsxF%lQA2>ru>%!N z_p_-ZQOuUdrzoLWK7KJ6bG)iQh$>s5M}GLQv_@3XuR#B7IH6cKBNT`z%+c!{<+g2`t72Wlnq%4IZQiml~q`jg^zf3&9IT_dx^KZZ)!(>v}wCAf1nu(q2Bo4g3jTAeo8m<@3?J@@g$!>S-PmL>3mpeYa=g~_}0iAfNh15(U?( zL`lCSRf-LJ>?l=hjE9FM9W#nfaMz|Fe>WB>GZ~x~q3P`=nyvsWOoP}e_~u1bpZnZW zGo1|$x*<~YT@$TidT!`VTZ11n+s2AIaJSI~KiW-h%uQT0GQ#cY{AH#uK4Z5lWqjDD z<-C+X+XVPV<+0V0p@{N(w$rD%>ei9(FWySkR^%RcajzhMsx+;Jxm4yieXy%h|Km5P zQi`ZKoNc5f9%fv}E6H0mv=NI9f>RIEe45`u%JBJiy_luNet}#7LOMlx?*M@9YdAZ8a-Ej>{ z+9gm5W?s(ffyoXb6JcTAu|WmHlW9w6!{5#7SVCIDhNPmdA|=0GT%%$;Z0T;r)_W;4 zdG`yHjFK*Z$Ge-)x#Z?BHnyh{uiZUZSZ*=>?GZo+ocDRRYVk@49_C{OZhQ764uimt zgLVu8pAKz?+%Og42!3$D@p3MQru(ZSXooO8(>$04wp!Yh{QK5=wUv&|Gvr37->)h#hjp0)+I~`_4z%nT&CEF{xako1eyo2oF0P)Sx{-)1oy1jM!;mA=lGKWE1n_UjwE``<{3C^ScannMnF+eqKBE3Wrb^DLlZrcrmVfEi4 zggyd7SW0gCl>D?FT}~_E=20I35~?*t-1x^h0_%q3W;V(A-xulhiqoXn!2hKQ5JR(+ zjGi}G;bC%CU^s8v&~`|_5GgZBP?A@IHa+()w$FRbtOZb>U$?Lgi7=ImbR0QApO(vT|ud}@(-)l`XqLXf)HNc@@!#MBrvt($`Q zckA;GZ!Z688hA&9b(05P z^BReJlccoed@7(iwL?!=vqie|yeP#xV!{XQAya*nac+0P(yQ!$RI}>7qtGC{NR~~9 zDrHf$HazDC^l|x%liL)DqpphI9fqD<7V0GW{}+g={O7|T%>-(KboN5e#&MG_Kd+|m z96b=nz=L_OBTbc_s-cz}k3XjQ{)#rTNRgT*m2JtGE^kM71#HriI)u}mVce%DrX(b} z)o_F*5a)kh&&Bod-)ZV!AEx~Zx&+`qHPbQC?}oy=sgK=lQR91*?>Kxh+K=Z>21Q zv7raGuBOVCcQ`LlXWb#c$C-chQ>u&VSPz*2jvoc>rCT@F_rhq34R_R<(tpA#iZD!IO##AYJ=NP*Y48h6l zS#z;qFyjK_mCo1^EyeHGh7!_;WZZP6{+o83&>XnObizNnA=t3#C{gnGJ<*K6Npc$` z$X&;k_>vUBiNSE>1J?gQ_Fnd6=gs(lEa4`ymI|&}dj}07U@HL-P}$t$_YIDlUmu?L ze==j|N<=7i!2c&|i7m=2wR1wGG&t^hPPrSp-wxnftt_|WOqA5!_0oBaZcoiresqD= z-q`Jc)@f8nRcrLl`93%`n#W$j5@xkEX=*HvF-3N1;9=8(mjE5F`_sQIyrXB9$S6%Y zx9=I||CNe(ne=O7;RCA_!aWu^Ve|Qy#P!zX824cFFF{R7QRh5LI8iY*DHAELj8MMn z!S+NKCAu=jHcyZ}KZDCEZO|PGjv3_3mkQs1Ut^a^vt#81V(M&lTm6<)CaF=m)3MLV z!5@hooP#%ZzX{Q4RPc~((`EyA&(3US#HmzGGm{+YfY+2uiYIlPrFj-zC~8b-?}^Q-#(I#@AUwFn5QffJR!;Y?2$)Hw9ufy#o#6vR6jw%JV+$ z9%Qvi9~&!Kch{Gs40`|^K(u$l)%T<3$5J!p#bb9>s!v|qzLSoy5M1llA#phjuD3mM z_zA;27zpX81AptiU%vM3H@D9!yibSRLi%9<@ob3m!d2LO#1Yq$s`ING4R-7}CN<&? zdxZ%@m1tMf>m087Q>7V!1qchfH_e+8Ovm#g*E`r@pX4^1lBL?a{=)O^snH~y`lh5} ze|@3S+O*DlE;ax6yUSd`)zI2*f7oO;@OvM)e5(;^GjuBB?UH_P_@57)phoW1if_l{g0Kx-6rGqfD zpifjkdKtTWlUEm{V1yDG5PpEoRDM`zH&r=__27HkCd}QO$*HKLM-grBNeli)W$Sr_ ze>8@zrC2_~;>_AB9g#yUxn@22m73Jc>6&T5@fcZuuatcfIFj9bo@f zcwWuz84-_Dvl(%Zi$mxN{;r+kGXnLrj~pXky_$clz~-}otG(dz3%!YF{710YFA**u z=cS@3$6KV=rXo%X>ZK#T9nn(Jw4WBW**V2m+8#nt+HO+_OtDu(@vN6B^1iq)#%2o$ z%!3Sdv1eR|bzfOX&DJy&-H%P%1SoEWk)D)@1666^0i{~vL7UL|;p%LCTR1&69)7YIDfK2rt9}xE*3Htzz>c{p0rv|_k0-|jyDTr`8d{*Fi zx`?*Ky;>7pB-847s!!&nyjme1%Dy%e8KzlbuKF& zM6KWJlB-r@+Ea~U9U*h3AB0GQA!SO-S+n1GcJ2ng8oHeo2%xZN_PJYK!b*iLJyijV zYWdUIb;8}M>M=o7u~o^Dj_(>1Z5G4#2Y5o?_xaYHHNIB>cbl$+6_)TR_`#d||5Lr$ zp(a~HV&V?_3#tV~r8$e1f#LZ* zTW3cc0@EhG@CR4YZ(plBV0puS)&n$`7a|&*@aO&i6RG7AU5)X1Jtf6YY{c$oJ%Uuv z7~XnTQyw)&enrd5Y~_~NDHHc~ezTzFk~MoXDRFTeTQ=WyIbnbEOkeKq%-VM&e%NoXjdYPSi-mH1vVF|;S6^g{xo{An7<(@Cc--q z!55hu%2SuQ{FiYd#^%&&I1_&<`v4hm0j2A*G{R}o3r24J*f8F`()#+W5xn$uX72k% z{<1SFw>Hnz+5~nOL>naWud5VJoN*p@S>DZwYo0q5R5*-KSRsa||FvIktt9xKnB_{trOVA$yn2-WaQm~ih7sbAtB?UcU; zJ-ozQ57AdvuO02X^X*wx(UT$fzb&Su=C62IfhGD2ZVPmNCER1V#m!V(qPWXYux(kW}3#&(g@p3qcL-p)ymp=Fu>$H%n56#@vyR^|SMF>3lc*gx&-^zTBOhN?Y^7| z8~(l%+OI!}vDY#*=TZ@*12hxS@YmjZQR_^jX!Y=5WAdt3S+u#dxow1-Sk0Yz6>9lX zE=6C6RKV%_SVOiBHlrAEfcBh;TroS=Znq=KyWY`yjmX(&`x1Bj_3?lw-v&po3>{A| zHkr*^fWEaDb7B7#Yd$VLbgiJf?h$CE;fC}5#Knie=Ec_Bp3}zL*wUJ39!1udy%HWB zLxh?TIa7U0kRxZjA%_=ZM8*`8L#wdMQ99Ge8_%l-JWJbpNaXE~#C<-k^B&logaNPX z)%NvgndGjTdHmpw{-r6^WVfN?Z=Z|-wym?6t&bHQC(Aaveva#8AB*A(?$V*Ps-bjO z%_rdIY2?_klLx0m$_lXsytlI#;_h|Gnn^WN^BG3vg~sxx!uW8N*xx^dgkxIdCy zp~4OSdG|J{Nw*M2l(#zEK7y`-1_$17ZC7CC&OrBeo~z6FrE52)sVv%`h>Ex_PA8Mq zH#8!UsrI(xNqhBtLC{C!y;71;>nhsP_v#94tyeE5q1bckWSnZm@k$c8(L8#ZL)_`a zsnZQ(w@yPM`v2VJuH{+7hurSBC5u1Q`om|)LtL7NV@v>}ytaL?<(OJ7w058+touC_ zZw3;jD2eOW^JeYnlItJ#BJYXbOAOG*CgEOFo%ibw3Mg6i-$CI?rM9huXpn0=eGpNt zx0V;8{YhR)(?sN1>K~t>811!;1KTr>{d+x-cPln@%*U*;R#!YViTC?6+P0qNy0a#p z_T-ei@Vg%_#dp+`)!~wTm5Q-IIGVmPlqbs-0)GE|$ z2MQ0yWRxhDp6b$L0>3LT!gnh^NS^v=gA?v~SP854SmqA?`y&57lwrs`J5`pw{b;l} z2z-LG>Dx#8l%m8|*2>Oo{5mHf>&VIpof>4qqnu{Fx3Z->Qak+{RqNsPS920yO9T%C z26r317e<9~qnYO2n)zmpoTmJC8ZNHY(Jsl2o^@VdGe~tloU_slG?S&SHo=kac&d18 z7=5z#T7>7F{Mmpv)U3n_@IEL$Q&kZ41JR^S2#;Q?*%vcf?o9@)f+k(v`FoIo2PeEo znf=(;HQ8W$rW_4M0kM+*@i6&(@n zinZW100ULZ;Z=p!f2QL%BUiY-7z}eX)TD5zgIdKiNmZ08?WNBf=HWd;bR-X86?r literal 0 HcmV?d00001 diff --git a/ai-capabilities/demo-ai-face-detector/src/components/CutosTopbar.vue b/ai-capabilities/demo-ai-face-detector/src/components/CutosTopbar.vue new file mode 100644 index 0000000..2f23f16 --- /dev/null +++ b/ai-capabilities/demo-ai-face-detector/src/components/CutosTopbar.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/ai-capabilities/demo-ai-face-detector/src/main.ts b/ai-capabilities/demo-ai-face-detector/src/main.ts new file mode 100644 index 0000000..2425c0f --- /dev/null +++ b/ai-capabilities/demo-ai-face-detector/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import './style.css' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/ai-capabilities/demo-ai-face-detector/src/style.css b/ai-capabilities/demo-ai-face-detector/src/style.css new file mode 100644 index 0000000..dc16bec --- /dev/null +++ b/ai-capabilities/demo-ai-face-detector/src/style.css @@ -0,0 +1,10 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; +:root{font-family:Inter,"Segoe UI",Arial,sans-serif;color:#20262c;background:#eef1f3;font-synthesis:none}*{box-sizing:border-box}body{margin:0;min-width:320px;min-height:100vh}button{font:inherit;cursor:pointer}button:disabled{cursor:not-allowed;opacity:.48}.app-shell{min-height:100vh;background:#eef1f3}main{width:100%;padding:28px clamp(18px,2vw,36px) 32px}.error-banner{display:flex;gap:12px;margin-bottom:20px;padding:12px 16px;color:#7e2420;background:#fff0ef;border-left:4px solid #d54d47}.section-heading{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:15px}.section-heading h2{margin:3px 0 0;font-size:18px}.eyebrow{color:#64717a;font:11px "Cascadia Code",Consolas,monospace}.dependency-tag{padding:6px 9px;color:#5d6870;background:#f8fafb;border:1px solid #cbd3d8;border-radius:4px;font:11px "Cascadia Code",Consolas,monospace}.dependency-tag strong{color:#253038}.result-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.result-card{min-width:0;min-height:260px;background:#fff;border:1px solid #d8dee2;border-radius:6px;overflow:hidden}.camera-card,.full-row{grid-column:span 2}.camera-card{min-height:0}.compact-card{min-height:220px}.detector-card pre{max-height:105px}.result-header{min-height:66px;padding:13px 15px;border-bottom:1px solid #e4e8eb;background:#f8fafb}.result-header h3{margin:0 0 6px;font-size:14px}.result-header code{display:block;color:#68757d;font-size:11px;overflow-wrap:anywhere}.method-header{display:flex;align-items:center;justify-content:space-between;gap:14px}.state-tag{max-width:55%;padding:5px 8px;color:#8b3c37;background:#fff0ef;border-radius:3px;font:10px "Cascadia Code",Consolas,monospace;text-align:right}.state-tag.online{color:#087b71;background:#e8f7f4}.camera-stage{position:relative;width:min(100%,960px);margin:0 auto;background:#111820;line-height:0;overflow:hidden}.camera-stage.empty{min-height:280px}.camera-stage video{display:block;width:100%;height:auto}.face-guide{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}.guide-shade{fill:rgba(7,12,17,.48)}.guide-ring{fill:none;stroke:#fff;stroke-width:3;stroke-dasharray:12 8;vector-effect:non-scaling-stroke;filter:drop-shadow(0 1px 2px rgba(0,0,0,.6))}.guide-ring.accepted{stroke:#42c6a8;stroke-dasharray:none}.camera-empty{position:absolute;inset:0;display:grid;place-items:center;color:#9da8ae;font-size:13px;line-height:1.4}.face-box{position:absolute;border:2px solid #e06b65;box-shadow:0 0 0 1px rgba(0,0,0,.25)}.face-box.accepted{border-color:#42c6a8}.face-box span{position:absolute;left:-2px;bottom:100%;padding:3px 6px;color:#fff;background:#e06b65;font:10px "Cascadia Code",Consolas,monospace;line-height:1.4}.face-box.accepted span{background:#138b80}.card-body{padding:15px}.button-row{display:flex;align-items:center;gap:9px;flex-wrap:wrap}.button{min-height:34px;padding:7px 13px;border:1px solid transparent;border-radius:4px;font-weight:650;font-size:12px}.button.primary{color:#fff;background:#138b80;border-color:#138b80}.button.secondary{color:#2d373d;background:#fff;border-color:#bfc8cd}pre{margin:0;padding:15px;max-height:280px;font:12px/1.55 "Cascadia Code",Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere;overflow:auto}.quality-list{padding:8px 15px}.quality-list div{display:flex;justify-content:space-between;gap:15px;padding:11px 0;border-bottom:1px solid #edf0f2;font-size:12px}.quality-list div:last-child{border:0}.quality-list b{color:#8b3c37;font:10px "Cascadia Code",Consolas,monospace}.quality-list b.pass{color:#087b71}.face-preview{height:250px;display:grid;place-items:center;padding:15px;color:#77838b;font-size:12px;background:#f5f7f8}.face-preview img{max-width:100%;max-height:220px;object-fit:contain;border:1px solid #d8dee2}.activity-list{max-height:280px;overflow:auto}.activity-list div{display:grid;grid-template-columns:78px minmax(0,1fr);gap:10px;padding:9px 13px;border-bottom:1px solid #edf0f2;font-size:12px}.activity-list time{color:#77838b;font-size:11px}.activity-list p{padding:8px 15px;color:#77838b;font-size:12px}@media(max-width:700px){.section-heading{align-items:flex-start;flex-direction:column}.result-grid{grid-template-columns:1fr}.camera-card,.full-row{grid-column:auto}.camera-stage.empty{min-height:200px}.state-tag{max-width:50%}} +.face-marker rect{fill:none;stroke:#e06b65;stroke-width:2;vector-effect:non-scaling-stroke}.face-marker.accepted rect{stroke:#42c6a8}.score-badge{position:absolute;top:10px;left:10px;padding:4px 7px;color:#fff;background:#e06b65;border-radius:3px;font:11px/1.4 "Cascadia Code",Consolas,monospace}.score-badge.accepted{background:#138b80}@media(orientation:portrait){.camera-stage.portrait{width:min(100%,calc(100vh - 300px));min-width:280px;aspect-ratio:1}.camera-stage.portrait video{width:100%;height:100%;object-fit:cover}.camera-stage.portrait.empty{min-height:0}} +.face-marker .confidence-bg{fill:#e06b65;stroke:none}.face-marker.accepted .confidence-bg{fill:#138b80;stroke:none}.confidence-text{fill:#fff;font:600 20px "Cascadia Code",Consolas,monospace}.video-status{position:absolute;z-index:2;top:12px;left:12px;right:12px;display:grid;grid-template-columns:minmax(150px,220px) minmax(260px,360px);justify-content:space-between;gap:10px;pointer-events:none}.video-panel{padding:9px 11px;color:#f7fafb;background:rgba(16,23,28,.72);border:1px solid rgba(255,255,255,.22);border-radius:5px;backdrop-filter:blur(4px);font-size:11px;line-height:1.35}.video-panel header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:6px}.video-panel strong{font-size:12px}.video-panel b{color:#f29a94;font:10px "Cascadia Code",Consolas,monospace}.video-panel b.pass{color:#55d8ba}.detector-panel>span{color:#c9d1d6;font:10px "Cascadia Code",Consolas,monospace}.gate-panel>div{display:grid;grid-template-columns:repeat(4,1fr);gap:5px}.gate-panel>div span{padding:3px 4px;color:#d99a96;text-align:center;background:rgba(196,71,64,.18);border-radius:3px;font:9px "Cascadia Code",Consolas,monospace}.gate-panel>div span.pass{color:#76dfc8;background:rgba(19,139,128,.24)}@media(max-width:700px){.video-status{grid-template-columns:1fr 1.5fr;top:8px;left:8px;right:8px;gap:6px}.video-panel{padding:7px 8px}.gate-panel>div{gap:3px}.gate-panel>div span{padding:3px 2px;font-size:8px}} +.video-status{grid-template-columns:minmax(220px,310px) minmax(260px,360px)}.video-panel{background:rgba(16,23,28,.42);border-color:rgba(255,255,255,.3);text-shadow:0 1px 2px rgba(0,0,0,.8)}.video-panel dl{margin:0}.video-panel dl div{display:grid;grid-template-columns:minmax(82px,.8fr) minmax(0,1.5fr);gap:8px;padding:3px 0;border-top:1px solid rgba(255,255,255,.14)}.video-panel dt,.video-panel dd{margin:0;padding:0;border:0}.video-panel dt{color:#d5dde1}.video-panel dd{min-width:0;color:#fff;font:10px/1.35 "Cascadia Code",Consolas,monospace;overflow-wrap:anywhere;text-align:right}.video-panel dd.pass{color:#70e0c6}.gate-panel dl div{grid-template-columns:minmax(0,1fr) 42px}@media(max-width:700px){.video-status{grid-template-columns:1fr 1fr}.video-panel dl div{grid-template-columns:1fr;padding:2px 0}.video-panel dd{text-align:left}.gate-panel dl div{grid-template-columns:minmax(0,1fr) 38px}.gate-panel dl dd{text-align:right}} +.compact-face-card{min-height:0}.compact-face-card+.detection-result-card{min-height:0}.compact-face-card{grid-column:1}.detection-result-card{grid-column:2}.face-preview{height:auto;min-height:0;padding:10px}.face-preview img{display:block;width:100%;height:auto;max-height:190px;object-fit:contain}.activity-card{min-height:0}.activity-card .result-header{min-height:56px;padding-top:10px;padding-bottom:10px}.activity-list{max-height:140px}.activity-list div{padding-top:6px;padding-bottom:6px}@media(min-width:701px){.result-grid{grid-template-columns:minmax(220px,32%) minmax(0,68%)}}@media(max-width:700px){.compact-face-card,.detection-result-card{grid-column:auto}.face-preview img{width:auto;max-width:100%;max-height:170px}.activity-list{max-height:110px}} +.video-panel{padding:6px 9px}.video-panel header{margin-bottom:3px}.video-panel dl div{padding:1px 0}.locked-face{position:absolute;z-index:3;right:12px;bottom:12px;margin:0;padding:5px;background:rgba(16,23,28,.46);border:1px solid rgba(255,255,255,.42);border-radius:5px;box-shadow:0 3px 12px rgba(0,0,0,.35)}.locked-face img{display:block;width:auto;height:auto;max-width:150px;max-height:180px;object-fit:contain}.locked-face figcaption{padding:4px 2px 0;color:#fff;font:9px/1.3 "Cascadia Code",Consolas,monospace;text-align:center}.detection-result-card,.activity-card{grid-column:auto;min-height:0}.detection-result-card pre,.activity-list{max-height:180px}@media(min-width:701px){.result-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:700px){.locked-face{right:8px;bottom:8px}.locked-face img{max-width:105px;max-height:130px}.detection-result-card pre,.activity-list{max-height:120px}} +.gate-panel dl{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 10px}.gate-panel dl div{grid-template-columns:minmax(0,1fr) 38px}.video-panel dt{font-size:9px}.video-panel dd{font-size:9px}@media(max-width:700px){.gate-panel dl{grid-template-columns:1fr 1fr;gap:0 5px}.gate-panel dl div{grid-template-columns:1fr}.gate-panel dl dd{text-align:left}} diff --git a/ai-capabilities/demo-ai-face-detector/src/utils/config.ts b/ai-capabilities/demo-ai-face-detector/src/utils/config.ts new file mode 100644 index 0000000..c3221fa --- /dev/null +++ b/ai-capabilities/demo-ai-face-detector/src/utils/config.ts @@ -0,0 +1,38 @@ +export interface LwaConfig { + name: string + version: string + description?: string + params: { + title: string + host: string + [key: string]: unknown + } +} + +const fallback: LwaConfig = { + name: 'demo-ai-face-detector', + version: '0.2.4', + params: { title: 'CUTOS AI Face Detector', host: 'localhost' } +} + +function mergeQueryParams(config: LwaConfig) { + const params = new URL(window.location.href).searchParams.get('params') + if (!params) return config + try { + config.params = { ...config.params, ...JSON.parse(params) } + } catch (error) { + console.error('Invalid LWA params', error) + } + return config +} + +export async function loadConfig(): Promise { + try { + const response = await fetch(new URL('config.json', window.location.href), { cache: 'no-cache' }) + if (!response.ok) throw new Error(`Failed to load config.json: ${response.status}`) + return mergeQueryParams(await response.json() as LwaConfig) + } catch (error) { + console.error(error) + return mergeQueryParams({ ...fallback, params: { ...fallback.params } }) + } +} diff --git a/ai-capabilities/demo-ai-face-detector/src/vite-env.d.ts b/ai-capabilities/demo-ai-face-detector/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/ai-capabilities/demo-ai-face-detector/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/ai-capabilities/demo-ai-face-detector/tailwind.config.js b/ai-capabilities/demo-ai-face-detector/tailwind.config.js new file mode 100644 index 0000000..940e6e2 --- /dev/null +++ b/ai-capabilities/demo-ai-face-detector/tailwind.config.js @@ -0,0 +1,6 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], + theme: { extend: {} }, + plugins: [] +} diff --git a/ai-capabilities/demo-ai-face-detector/tsconfig.json b/ai-capabilities/demo-ai-face-detector/tsconfig.json new file mode 100644 index 0000000..99418ba --- /dev/null +++ b/ai-capabilities/demo-ai-face-detector/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "baseUrl": ".", + "paths": { "@/*": ["src/*"] }, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "jsx": "preserve", + "types": ["vite/client"] + }, + "include": ["src/**/*.ts", "src/**/*.vue"] +} diff --git a/ai-capabilities/demo-ai-face-detector/vite.config.ts b/ai-capabilities/demo-ai-face-detector/vite.config.ts new file mode 100644 index 0000000..c653100 --- /dev/null +++ b/ai-capabilities/demo-ai-face-detector/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import AutoImport from 'unplugin-auto-import/vite' + +export default defineConfig({ + base: './', + publicDir: 'public', + build: { target: ['chrome74'] }, + resolve: { alias: { '@': '/src' } }, + plugins: [ + vue(), + AutoImport({ + include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/], + imports: ['vue'], + vueTemplate: true, + cache: true, + dts: false + }) + ], + optimizeDeps: { + esbuildOptions: { + define: { global: 'globalThis' }, + target: 'es2015', + supported: { bigint: true } + } + } +}) diff --git a/ai-capabilities/demo-ai-face-service/README.md b/ai-capabilities/demo-ai-face-service/README.md new file mode 100644 index 0000000..875fa11 --- /dev/null +++ b/ai-capabilities/demo-ai-face-service/README.md @@ -0,0 +1,20 @@ +# Demo AI Face Service + +This LWA demonstrates the complete browser-to-Gateway face workflow: + +1. `@cutos/ai-face-detector` captures one qualified face locally. +2. `@cutos/gw-client` connects with the CUTOS device credentials obtained from `@cutos/core`. +3. `@cutos/ai-face-service` creates the selected collection and invokes registration, lookup, listing, search, comparison, removal, and clearing. + +During SDK development, the demo references `file:../sdk`. After `@cutos/ai-face-service` is published, replace it with `^4.0.0` and regenerate `package-lock.json`. + +## Run + +```bash +npm install +cutos lwa build +cutos lwa upload --upload-mode oss +cutos lwa publish --device +``` + +The target CUTOS Runtime must provide device information including Gateway credentials (`id`, `gwi`, `token`, and `gwUrl`). diff --git a/ai-capabilities/demo-ai-face-service/index.html b/ai-capabilities/demo-ai-face-service/index.html new file mode 100644 index 0000000..b2ffa15 --- /dev/null +++ b/ai-capabilities/demo-ai-face-service/index.html @@ -0,0 +1,13 @@ + + + + + + CUTOS AI Face Service + + +
+ + + + diff --git a/ai-capabilities/demo-ai-face-service/package.json b/ai-capabilities/demo-ai-face-service/package.json new file mode 100644 index 0000000..d8cc128 --- /dev/null +++ b/ai-capabilities/demo-ai-face-service/package.json @@ -0,0 +1,32 @@ +{ + "name": "demo-ai-face-service", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "assets": "cutos-ai-face-detector-assets public/cutos-ai-face-detector", + "dev": "npm run assets && vite", + "build": "cutos lwa build", + "build:app": "npm run assets && vue-tsc --noEmit && vite build", + "package": "cutos lwa package", + "validate": "cutos lwa validate", + "preview": "vite preview" + }, + "dependencies": { + "@cutos/ai-face-detector": "^4.0.1", + "@cutos/ai-face-service": "file:../sdk", + "@cutos/core": "^4.0.8", + "@cutos/gw-client": "^1.0.9", + "vue": "^3.4.31" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.5", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.40", + "tailwindcss": "^3.4.7", + "typescript": "^5.5.4", + "unplugin-auto-import": "^0.18.0", + "vite": "^5.3.4", + "vue-tsc": "^2.0.26" + } +} diff --git a/ai-capabilities/demo-ai-face-service/postcss.config.js b/ai-capabilities/demo-ai-face-service/postcss.config.js new file mode 100644 index 0000000..75af576 --- /dev/null +++ b/ai-capabilities/demo-ai-face-service/postcss.config.js @@ -0,0 +1,7 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +} + diff --git a/ai-capabilities/demo-ai-face-service/public/config.json b/ai-capabilities/demo-ai-face-service/public/config.json new file mode 100644 index 0000000..19ae1fb --- /dev/null +++ b/ai-capabilities/demo-ai-face-service/public/config.json @@ -0,0 +1,9 @@ +{ + "name": "demo-ai-face-service", + "version": "0.2.0", + "description": "CUTOS 4.0 Gateway AI face service demo", + "params": { + "title": "CUTOS AI Face Service", + "host": "localhost" + } +} diff --git a/ai-capabilities/demo-ai-face-service/public/thumbnail.png b/ai-capabilities/demo-ai-face-service/public/thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..718a552033fef6c76f1939b8cdccff9a48f334f4 GIT binary patch literal 13163 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

!8k?t+OeeIlT2 z^L^#K2xJga=wdlYv=XCvW(wCrp8R0#$id@vNU9e8lM7@>-A#w1n5)idV7x8#Dt zhO0in`nPna9TV|#?T&@g2e%bA%NcXNuAv?|mBZK&uYO{^5~|9a+D`b~Q{K{GLa?ii zrFxo^L$MuJnCpqTE10CI3KHwYn=D#4EyPjdL`6cG8~~L90-Z=tUI$t%yg-P(dz6Bd zH-n1ZaN8i4Hs(sYN(ZTCB28N+Wh}4Iwcs@ zx?Y4P910(RTO9UM7XQdr*arau+CVI3dbr30vY_glbqwZ?kt~Fobw!vx;ap< zxS+V`|8gKO`P*IEP&wPEXEk@oX2z>63(w-^P40M^)XH>KYpv7~e|u%tTYivXVZS2{ zN;y-nots|i0WJC`%P-^|UM58tiC(|#(S{nhJ7E-4nqJq#@n(QtT(P{wWK&j(jo8L^cLg!RgdGB6Ueby{|c^>STMkZJp%HyBWgee8eiQC^jt`=g#qrHOU3_a~loR!Dd zZfnT2Tf4`-RCJETd%ErH$>y_lsNa_KhizScmO1t&Bo-(3whaKGq{{=KCWBfRa9N +import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue' +import { CoreAPI } from '@cutos/core' +import { FaceDetector, type FaceDetectionResult } from '@cutos/ai-face-detector' +import { FaceService, type FaceSearchResult } from '@cutos/ai-face-service' +import { IPC } from '@cutos/gw-client' +import { loadConfig } from './utils/config' + +interface DeviceInfo { + id: string + gwi: string + token: string + gwUrl: string + name?: string +} +interface ActivityItem { id: number; time: string; message: string } +interface ServiceNotice { tone: 'success' | 'empty'; message: string } + +const video = ref(null) +const title = ref('CUTOS AI Face Service') +const host = ref('localhost') +const lwaName = ref('demo-ai-face-service') +const lwaVersion = ref('0.1.0') +const coreVersion = ref('') +const runtimeConnected = ref(false) +const gatewayConnected = ref(false) +const cameraActive = ref(false) +const busy = ref('') +const errorText = ref('') +const latest = ref(null) +const capturedImage = ref('') +const registeredId = ref('') +const userId = ref('demo-user') +const serviceResult = ref(null) +const serviceNotice = ref(null) +const activity = ref([]) +const autoCaptureProgress = ref(0) +const restartCountdown = ref(10) + +let detector: FaceDetector | null = null +let gateway: IPC | null = null +let faceService: FaceService | null = null +let stream: MediaStream | null = null +let animationFrame = 0 +let inferenceRunning = false +let stableSince = 0 +let stableBox: FaceDetectionResult['primary'] = null +let restartTimer = 0 + +const STABILITY_DURATION_MS = 2000 +const FACE_MATCH_THRESHOLD = 0.5 + +const faceStatus = computed(() => { + if (!latest.value?.primary) return 'No face detected' + if (!latest.value.quality.accepted) return latest.value.quality.reasons.join(' · ') + if (capturedImage.value) return `Face captured · restart in ${restartCountdown.value}s` + if (stableSince) return `Hold still · ${Math.round(autoCaptureProgress.value * 100)}%` + return 'Face ready to capture' +}) +const serviceStatus = computed(() => gatewayConnected.value ? 'CONNECTED' : 'WAITING') +const guideCircle = computed(() => { + const frame = latest.value?.frame + if (!frame) { + return { viewBox: '0 0 100 100', cx: 50, cy: 50, radius: 43 } + } + return { + viewBox: `0 0 ${frame.width} ${frame.height}`, + cx: frame.width / 2, + cy: frame.height / 2, + radius: Math.min(frame.width, frame.height) * 0.43, + } +}) +function format(value: unknown) { return value == null ? 'No data' : JSON.stringify(value, null, 2) } +function log(message: string) { + activity.value.unshift({ id: Date.now() + Math.random(), time: new Date().toLocaleTimeString(), message }) + activity.value = activity.value.slice(0, 20) +} +async function run(name: string, action: () => Promise) { + busy.value = name + errorText.value = '' + serviceNotice.value = null + try { await action() } catch (error) { + errorText.value = error instanceof Error ? error.message : String(error) + log(`${name}: ${errorText.value}`) + } finally { busy.value = '' } +} +async function initializeRuntimeAndGateway() { + await CoreAPI.init(host.value) + runtimeConnected.value = CoreAPI.connected() + coreVersion.value = CoreAPI.getVersion() + const info = await CoreAPI.getDeviceInfo() + if (!info?.id || !info.gwi || !info.token || !info.gwUrl) { + throw new Error('CUTOS device information does not include Gateway credentials.') + } + gateway = await new Promise((resolve, reject) => { + const ipc = new IPC(info.gwUrl, { gwi: info.gwi, username: info.id, token: info.token }, status => { + if (status.status) resolve(ipc) + else reject(new Error(status.msg || 'Gateway connection failed.')) + }) + }) + faceService = new FaceService(gateway, { lwaName: lwaName.value }) + await faceService.init() + gatewayConnected.value = true + log(`Gateway connected for ${info.name || info.id}; face collection is ${lwaName.value}`) +} +async function initializeDetector() { + if (detector) return + detector = await FaceDetector.create({ + assetBaseUrl: new URL('cutos-ai-face-detector', window.location.href).toString(), + preferredDelegate: 'gpu', + allowCpuFallback: true, + minIntervalMs: 180, + minDetectionConfidence: 0.9, + minFaceSizeRatio: 0.06, + maxHorizontalCenterOffsetRatio: 0.2, + guideAspectRatio: 1, + }) + log('Local face detector initialized') +} +async function startCamera() { + if (cameraActive.value) return + await run('start camera', async () => { + await initializeDetector() + stream = await navigator.mediaDevices.getUserMedia({ + video: { + width: { ideal: 720 }, + height: { ideal: 720 }, + aspectRatio: { ideal: 1 }, + facingMode: 'user', + }, + audio: false, + }) + cameraActive.value = true + await nextTick() + if (!video.value) throw new Error('Video element is unavailable.') + video.value.srcObject = stream + await video.value.play() + clearRestartCountdown() + capturedImage.value = '' + resetStability() + detectLoop() + log('Camera started') + }) +} +function stopCamera() { + clearRestartCountdown() + resetStability() + cancelAnimationFrame(animationFrame) + stream?.getTracks().forEach(track => track.stop()) + stream = null + if (video.value) video.value.srcObject = null + cameraActive.value = false + latest.value = null + log('Camera stopped') +} +async function detectLoop() { + if (!cameraActive.value || !video.value || !detector || capturedImage.value) return + if (!inferenceRunning) { + inferenceRunning = true + try { + const next = await detector.detect(video.value) + latest.value = next + updateStability(next) + } + catch (error) { errorText.value = error instanceof Error ? error.message : String(error) } + finally { inferenceRunning = false } + } + if (!capturedImage.value) animationFrame = requestAnimationFrame(detectLoop) +} +function resetStability() { + stableSince = 0 + stableBox = null + autoCaptureProgress.value = 0 +} +function updateStability(next: FaceDetectionResult) { + const face = next.primary + if (!next.quality.accepted || !face) { + resetStability() + return + } + + const previous = stableBox?.box + const current = face.box + const stable = previous && + Math.hypot( + current.normalized.x + current.normalized.width / 2 - previous.normalized.x - previous.normalized.width / 2, + current.normalized.y + current.normalized.height / 2 - previous.normalized.y - previous.normalized.height / 2 + ) <= 0.035 && + Math.abs(current.normalized.width - previous.normalized.width) <= 0.04 && + Math.abs(current.normalized.height - previous.normalized.height) <= 0.04 + + if (!stable) { + stableSince = performance.now() + stableBox = face + autoCaptureProgress.value = 0 + return + } + + const elapsed = performance.now() - stableSince + autoCaptureProgress.value = Math.min(1, elapsed / STABILITY_DURATION_MS) + if (elapsed >= STABILITY_DURATION_MS) { + capturedImage.value = face.image + autoCaptureProgress.value = 1 + serviceResult.value = { captured: true, automatic: true, score: face.score, timestamp: new Date().toISOString() } + log('Stable face captured after 2 seconds') + startRestartCountdown() + } +} +function clearRestartCountdown() { + if (restartTimer) window.clearInterval(restartTimer) + restartTimer = 0 + restartCountdown.value = 10 +} +function startRestartCountdown() { + clearRestartCountdown() + restartTimer = window.setInterval(() => { + restartCountdown.value -= 1 + if (restartCountdown.value <= 0) detectAgain() + }, 1000) +} +function detectAgain() { + if (!cameraActive.value || !detector) return + clearRestartCountdown() + capturedImage.value = '' + latest.value = null + resetStability() + log('Detection restarted') + animationFrame = requestAnimationFrame(detectLoop) +} +function requireServiceAndImage(): FaceService { + if (!faceService || !gatewayConnected.value) throw new Error('Gateway face service is not connected.') + if (!capturedImage.value) throw new Error('Capture a qualified face first.') + return faceService +} +function requireService(): FaceService { + if (!faceService || !gatewayConnected.value) throw new Error('Gateway face service is not connected.') + return faceService +} +async function searchCapturedFace(service: FaceService): Promise { + try { + return await service.search(capturedImage.value) + } catch (error) { + if (error instanceof Error && /no match found/i.test(error.message)) return null + throw error + } +} +async function register() { + await run('register', async () => { + const service = requireServiceAndImage() + try { + registeredId.value = await service.register(capturedImage.value, { + userId: userId.value, + saveImage: true, + }) + } catch (error) { + if (!(error instanceof Error) || !/USER_ID_ALREADY_EXISTS/i.test(error.message)) throw error + const existing = await service.getByUserId(userId.value) + if (!existing) throw error + registeredId.value = existing.id + serviceResult.value = existing + serviceNotice.value = { + tone: 'success', + message: `User ID ${userId.value} is already registered. Face ID: ${existing.id}`, + } + log(`User ${userId.value} is already registered as ${existing.id}`) + return + } + serviceResult.value = { registeredId: registeredId.value, userId: userId.value, imageSaved: true } + serviceNotice.value = { tone: 'success', message: `Registered successfully. Face ID: ${registeredId.value}` } + log(`Registered face ${registeredId.value} for ${userId.value}`) + }) +} +async function search() { + await run('search', async () => { + const service = requireServiceAndImage() + const match = await searchCapturedFace(service) + if (match && match.score >= FACE_MATCH_THRESHOLD) { + registeredId.value = match.id + serviceResult.value = match + serviceNotice.value = { + tone: 'success', + message: `Face found. ID: ${match.id}, score: ${(match.score * 100).toFixed(1)}%`, + } + log(`Search match ${match.id}, score ${match.score.toFixed(3)}`) + } else { + registeredId.value = '' + serviceResult.value = match + serviceNotice.value = { tone: 'empty', message: 'No registered face matches the captured image.' } + log('No matching face found') + } + }) +} +async function unregister() { + await run('unregister', async () => { + const service = requireService() + if (!registeredId.value) throw new Error('No registered face id is available.') + const id = await service.unregister(registeredId.value) + serviceResult.value = { unregisteredId: id } + registeredId.value = '' + serviceNotice.value = { tone: 'success', message: `Face ${id} was unregistered.` } + log(`Unregistered face ${id}`) + }) +} +async function getByUserId() { + await run('get by user id', async () => { + const service = requireService() + const record = await service.getByUserId(userId.value, { includeImage: true }) + serviceResult.value = record + if (record) registeredId.value = record.id + else registeredId.value = '' + serviceNotice.value = record + ? { tone: 'success', message: `Face found. ID: ${record.id}` } + : { tone: 'empty', message: `No face is registered for ${userId.value}.` } + log(record ? `Loaded face ${record.id} for ${userId.value}` : `No face registered for ${userId.value}`) + }) +} +async function listFaces() { + await run('list faces', async () => { + const service = requireService() + const page = await service.list({ limit: 20 }) + serviceResult.value = page + log(`Listed ${page.items.length} face records`) + }) +} +async function clearFaces() { + if (!window.confirm(`Clear every face registered for ${lwaName.value}?`)) return + await run('clear faces', async () => { + const service = requireService() + await service.clear() + registeredId.value = '' + serviceResult.value = { cleared: true } + serviceNotice.value = { tone: 'success', message: 'All registered faces were cleared.' } + log(`Cleared face collection ${lwaName.value}`) + }) +} + +onMounted(async () => { + const config = await loadConfig() + title.value = String(config.params?.title || title.value) + host.value = import.meta.env.VITE_CUTOS_BROKER_URL || String(config.params?.host || host.value) + lwaName.value = String(config.name || lwaName.value) + lwaVersion.value = String(config.version || lwaVersion.value) + await run('initialize Gateway service', initializeRuntimeAndGateway) + await startCamera() +}) +onUnmounted(() => { + clearRestartCountdown() + stopCamera() + detector?.dispose() +}) + + + diff --git a/ai-capabilities/demo-ai-face-service/src/camera-overlay.css b/ai-capabilities/demo-ai-face-service/src/camera-overlay.css new file mode 100644 index 0000000..1442d06 --- /dev/null +++ b/ai-capabilities/demo-ai-face-service/src/camera-overlay.css @@ -0,0 +1,113 @@ +.camera { width: min(100%, 720px); aspect-ratio: 1; } +.camera video { width: 100%; height: 100%; object-fit: cover; } + +.camera .face-guide { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.camera .guide-shade { fill: rgba(7, 12, 17, 0.56); } + +.camera .guide-ring { + fill: none; + stroke: #fff; + stroke-width: 3; + stroke-dasharray: 12 8; + vector-effect: non-scaling-stroke; + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6)); +} + +.camera .guide-ring.ready { stroke: #42c6a8; stroke-dasharray: none; } + +.face-marker rect { fill: none; stroke: #e06b65; stroke-width: 2; vector-effect: non-scaling-stroke; } +.face-marker.accepted rect { stroke: #42c6a8; } +.face-marker .confidence-bg { fill: #e06b65; stroke: none; } +.face-marker.accepted .confidence-bg { fill: #138b80; stroke: none; } +.confidence-text { fill: #fff; font: 600 20px "Cascadia Code", Consolas, monospace; } + +.video-status { + position: absolute; + z-index: 2; + top: 12px; + left: 12px; + right: 12px; + display: grid; + grid-template-columns: minmax(220px, 310px) minmax(240px, 320px); + justify-content: space-between; + gap: 10px; + pointer-events: none; +} + +.video-panel { + color: #f7fafb; + background: rgba(16, 23, 28, 0.42); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 5px; + backdrop-filter: blur(4px); + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); +} + +.video-panel { padding: 6px 9px; font-size: 11px; line-height: 1.35; } +.video-panel header { min-height: 0; padding: 0; display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 0 0 3px; background: transparent; border: 0; } +.video-panel strong { font-size: 12px; } +.video-panel b { color: #f29a94; font: 10px "Cascadia Code", Consolas, monospace; } +.video-panel b.pass { color: #55d8ba; } +.gate-panel { grid-column: 2; } +.video-panel dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 10px; margin: 0; } +.video-panel dl div { display: grid; grid-template-columns: minmax(0, 1fr) 38px; gap: 8px; padding: 1px 0; border-top: 1px solid rgba(255, 255, 255, 0.14); } +.video-panel dt, +.video-panel dd { margin: 0; padding: 0; border: 0; } +.video-panel dt { color: #d5dde1; font-size: 9px; } +.video-panel dd { min-width: 0; color: #fff; font: 9px/1.35 "Cascadia Code", Consolas, monospace; overflow-wrap: anywhere; text-align: right; } +.video-panel dd.pass { color: #70e0c6; } + +.locked-face { position: absolute; z-index: 3; right: 12px; bottom: 12px; margin: 0; padding: 5px; background: rgba(16, 23, 28, 0.46); border: 1px solid rgba(255, 255, 255, 0.42); border-radius: 5px; box-shadow: 0 3px 12px rgba(0, 0, 0, 0.35); } +.locked-face img { display: block; width: auto; height: auto; max-width: 150px; max-height: 180px; object-fit: contain; } +.locked-face figcaption { padding: 4px 2px 0; color: #fff; font: 9px/1.3 "Cascadia Code", Consolas, monospace; text-align: center; } + +.identity-fields { + display: grid; + grid-template-columns: minmax(150px, 0.8fr) minmax(220px, 1.2fr); + gap: 12px; + padding: 14px; + border-bottom: 1px solid #edf0f2; +} + +.identity-fields label { display: grid; gap: 6px; min-width: 0; } +.identity-fields span { color: #68757d; font-size: 11px; font-weight: 600; } +.identity-fields input { + min-width: 0; + width: 100%; + height: 36px; + padding: 0 10px; + color: #253038; + background: #fff; + border: 1px solid #bcc6cc; + border-radius: 4px; + font: 12px "Cascadia Code", Consolas, monospace; +} +.identity-fields input[readonly] { color: #4e5b63; background: #f5f7f8; } + +.service-notice { + margin: 0; + padding: 10px 14px; + border-bottom: 1px solid #edf0f2; + font-size: 12px; + line-height: 1.45; +} +.service-notice.success { color: #087b71; background: #e8f7f4; } +.service-notice.empty { color: #8b3c37; background: #fff0ef; } + +@media (max-width: 700px) { + .identity-fields { grid-template-columns: 1fr; } + .video-status { top: 8px; left: 8px; right: 8px; grid-template-columns: 1fr 1fr; gap: 6px; } + .video-panel { padding: 7px 8px; } + .video-panel dl { gap: 0 5px; } + .video-panel dl div { grid-template-columns: 1fr; gap: 1px; padding: 2px 0; } + .video-panel dd { text-align: left; } + .locked-face { right: 8px; bottom: 8px; } + .locked-face img { max-width: 105px; max-height: 130px; } +} diff --git a/ai-capabilities/demo-ai-face-service/src/gw-client.d.ts b/ai-capabilities/demo-ai-face-service/src/gw-client.d.ts new file mode 100644 index 0000000..04ee569 --- /dev/null +++ b/ai-capabilities/demo-ai-face-service/src/gw-client.d.ts @@ -0,0 +1,18 @@ +declare module '@cutos/gw-client' { + export class IPC { + gwi: string + constructor( + gwUrl: string, + options: { gwi: string; username: string; token: string; ext?: Record }, + callback?: (status: { status: boolean; msg?: string }) => void, + ) + publishRequest( + topic: string, + body: unknown, + callback?: (response: unknown, error?: unknown) => void, + userContext?: Record, + options?: unknown, + ): void + } +} + diff --git a/ai-capabilities/demo-ai-face-service/src/main.ts b/ai-capabilities/demo-ai-face-service/src/main.ts new file mode 100644 index 0000000..966a7db --- /dev/null +++ b/ai-capabilities/demo-ai-face-service/src/main.ts @@ -0,0 +1,6 @@ +import { createApp } from 'vue' +import './style.css' +import './camera-overlay.css' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/ai-capabilities/demo-ai-face-service/src/style.css b/ai-capabilities/demo-ai-face-service/src/style.css new file mode 100644 index 0000000..987f811 --- /dev/null +++ b/ai-capabilities/demo-ai-face-service/src/style.css @@ -0,0 +1 @@ +:root{font-family:Inter,"Segoe UI",Arial,sans-serif;color:#20262c;background:#eef1f3;font-synthesis:none}*{box-sizing:border-box}body{margin:0;min-width:320px;min-height:100vh}button{padding:8px 12px;color:#2d373d;background:#fff;border:1px solid #bcc6cc;border-radius:4px;font:600 12px inherit;cursor:pointer}button.primary{color:#fff;background:#138b80;border-color:#138b80}button:disabled{cursor:not-allowed;opacity:.46}.app-shell{min-height:100vh}.topbar{min-height:82px;padding:14px clamp(18px,4vw,52px);display:flex;align-items:center;justify-content:space-between;gap:24px;color:#f8fafb;background:#20262c;border-bottom:4px solid #16a394}.brand{display:flex;align-items:center;gap:15px}.brand-mark{display:grid;place-items:center;width:46px;height:46px;color:#fff;border:1px solid #6ccbbb;border-radius:50%;font-size:10px;font-weight:800;letter-spacing:.7px}.brand h1{margin:0;font-size:20px}.brand p{margin:4px 0 0;color:#aeb8bf;font-size:12px}.tags{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}.tags span,.tags b{padding:7px 9px;background:#2b3339;border:1px solid #4b555d;border-radius:4px;font:11px "Cascadia Code",Consolas,monospace}.tags b{color:#eb9b96}.tags b.online{color:#55d8ba}main{padding:28px clamp(18px,2vw,36px) 32px}.error-banner{display:flex;gap:12px;margin-bottom:18px;padding:12px 15px;color:#7e2420;background:#fff0ef;border-left:4px solid #d54d47;font-size:13px}.heading{display:flex;align-items:end;justify-content:space-between;gap:16px;margin-bottom:15px}.heading small{color:#64717a;font:11px "Cascadia Code",Consolas,monospace}.heading h2{margin:3px 0 0;font-size:18px}.dependency{padding:6px 9px;color:#5d6870;background:#f8fafb;border:1px solid #cbd3d8;border-radius:4px;font:11px "Cascadia Code",Consolas,monospace}.dependency b{color:#253038}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.card{min-width:0;background:#fff;border:1px solid #d8dee2;border-radius:6px;overflow:hidden}.card header{min-height:64px;padding:13px 15px;display:flex;justify-content:space-between;align-items:center;gap:12px;background:#f8fafb;border-bottom:1px solid #e4e8eb}.card h3{margin:0 0 6px;font-size:14px}.card code{display:block;color:#68757d;font-size:11px;overflow-wrap:anywhere}.camera-card{grid-column:span 2}.state{max-width:52%;padding:5px 8px;color:#8b3c37;background:#fff0ef;border-radius:3px;font:10px "Cascadia Code",Consolas,monospace;text-align:right}.state.ready{color:#087b71;background:#e8f7f4}.camera{position:relative;width:min(100%,900px);margin:auto;background:#111820;line-height:0;overflow:hidden}.camera video{display:block;width:100%;height:auto}.camera>p{min-height:280px;margin:0;display:grid;place-items:center;color:#aab4ba;font-size:13px;line-height:1.4}.face-box{position:absolute;border:2px solid #e06b65}.face-box.ready{border-color:#42c6a8}.face-box span{position:absolute;left:-2px;bottom:100%;padding:3px 6px;color:#fff;background:#e06b65;font:10px "Cascadia Code",Consolas,monospace;line-height:1.4}.face-box.ready span{background:#138b80}.card footer,.service-actions{display:flex;gap:9px;flex-wrap:wrap;padding:14px}.service-actions{border-bottom:1px solid #edf0f2}.service-card dl{margin:0;padding:0 15px}.service-card dl div{display:flex;justify-content:space-between;gap:12px;padding:9px 0;border-bottom:1px solid #edf0f2;font-size:12px}.service-card dt{color:#68757d}.service-card dd{margin:0;max-width:60%;font-family:"Cascadia Code",Consolas,monospace;overflow-wrap:anywhere;text-align:right}pre{max-height:180px;margin:0;padding:14px;overflow:auto;font:12px/1.55 "Cascadia Code",Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.preview{min-height:210px;padding:12px;display:grid;place-items:center;background:#f5f7f8}.preview img{max-width:100%;max-height:260px;object-fit:contain;border:1px solid #d8dee2}.preview p{color:#77838b;font-size:12px;text-align:center}.quality-gates{padding:8px 15px}.quality-gates div{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:8px 0;border-bottom:1px solid #edf0f2;font-size:12px}.quality-gates div:last-child{border-bottom:0}.quality-gates b{color:#9a443e;font:10px "Cascadia Code",Consolas,monospace}.quality-gates b.pass{color:#087b71}.activity{max-height:265px;overflow:auto}.activity div{display:grid;grid-template-columns:78px minmax(0,1fr);gap:10px;padding:9px 13px;border-bottom:1px solid #edf0f2;font-size:12px}.activity time{color:#77838b;font-size:11px}.activity p{padding:8px 15px;color:#77838b;font-size:12px}@media(max-width:700px){.topbar,.heading{align-items:flex-start;flex-direction:column;gap:12px}.tags{justify-content:flex-start}.grid{grid-template-columns:1fr}.camera-card{grid-column:auto}.state{max-width:50%}.camera>p{min-height:200px}} diff --git a/ai-capabilities/demo-ai-face-service/src/utils/config.ts b/ai-capabilities/demo-ai-face-service/src/utils/config.ts new file mode 100644 index 0000000..8bc4095 --- /dev/null +++ b/ai-capabilities/demo-ai-face-service/src/utils/config.ts @@ -0,0 +1,6 @@ +export async function loadConfig(): Promise> { + const response = await fetch('./config.json', { cache: 'no-store' }) + if (!response.ok) throw new Error(`Unable to load config.json: ${response.status}`) + return response.json() +} + diff --git a/ai-capabilities/demo-ai-face-service/src/vite-env.d.ts b/ai-capabilities/demo-ai-face-service/src/vite-env.d.ts new file mode 100644 index 0000000..ed77210 --- /dev/null +++ b/ai-capabilities/demo-ai-face-service/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// + diff --git a/ai-capabilities/demo-ai-face-service/tailwind.config.js b/ai-capabilities/demo-ai-face-service/tailwind.config.js new file mode 100644 index 0000000..2cfac94 --- /dev/null +++ b/ai-capabilities/demo-ai-face-service/tailwind.config.js @@ -0,0 +1,6 @@ +export default { + content: ['./index.html', './src/**/*.{vue,ts}'], + theme: { extend: {} }, + plugins: [] +} + diff --git a/ai-capabilities/demo-ai-face-service/tsconfig.json b/ai-capabilities/demo-ai-face-service/tsconfig.json new file mode 100644 index 0000000..7decca0 --- /dev/null +++ b/ai-capabilities/demo-ai-face-service/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": false, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "types": ["vite/client"] + }, + "include": ["src/**/*.ts", "src/**/*.vue"] +} + diff --git a/ai-capabilities/demo-ai-face-service/vite.config.ts b/ai-capabilities/demo-ai-face-service/vite.config.ts new file mode 100644 index 0000000..48f0a7d --- /dev/null +++ b/ai-capabilities/demo-ai-face-service/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + base: './', + publicDir: 'public', + build: { target: ['chrome74'] }, + resolve: { alias: { '@': '/src' } }, + plugins: [vue()], + optimizeDeps: { + esbuildOptions: { + define: { global: 'globalThis' }, + target: 'es2015', + supported: { bigint: true } + } + } +}) +