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 0000000..83da7fa Binary files /dev/null and b/ai-capabilities/demo-ai-face-detector/public/thumbnail.png differ 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 0000000..9f73429 Binary files /dev/null and b/ai-capabilities/demo-ai-face-detector/src/assets/cutos.png differ 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 0000000..718a552 Binary files /dev/null and b/ai-capabilities/demo-ai-face-service/public/thumbnail.png differ diff --git a/ai-capabilities/demo-ai-face-service/src/App.vue b/ai-capabilities/demo-ai-face-service/src/App.vue new file mode 100644 index 0000000..5518427 --- /dev/null +++ b/ai-capabilities/demo-ai-face-service/src/App.vue @@ -0,0 +1,396 @@ + + + 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 } + } + } +}) +